8.2 使用 flex 对 TinyC 源文件进行词法分析

上一节的第二个例子 word-spliter 就是一个原始的分词器,在此例的框架上加以扩展就可以做为 TinyC 的词法分析器了。

word-spliter 中只有 WORD 这一种类型的 token ,所有连续的非空格字符串都是一个 WORD ,它的正则表达式非常简单: [^ \t\n\r\a]+ 。 该程序中为 WORD 类型的 token 定义了一个值为 1 的编号: T_WORD ,每当扫描出一个完整的 WORD 时,就向 main 函数返回 T_WORD ,遇到文件结束则返回 0 。 main 函数则根据 yylex 函数的返回值进行不同的处理。

从 word-spliter 程序的框架和流程中可以看出,词法分析器的扩展方法非常简单:

(1) 列出 TinyC 中所有类型的 token;

(2) 为每种类型的 token 分配一个唯一的编号,同时写出此 token 的正则表达式;

(3) 写出每种 token 的 rule (相应的 pattern 和 action )。

TinyC 中的 token 的种类非常少,按其词法特性,分为以下三大类。

第 1 类为单字符运算符,一共 15 种:

  1. + * - / % = , ; ! < > ( ) { }

第 2 类为双字符运算符和关键字,一共 16 种:

  1. <=, >=, ==, !=, &&, ||
  2. void, int, while, if, else, return, break, continue, print, readint

第 3 类为整数常量、字符串常量和标识符(变量名和函数名),一共 3 种。

除第 3 类 token 的正则表达式稍微麻烦一点外,第 1 、 2 类 token 的正则表达式就是这些运算符或关键字的字面值。

token 的编号原则为:单字符运算符的 token 编号就是其字符的数值,其他类型的 token 则从 256 开始编号。

各类 token 的正则表达式及相应的 action 见下面的 scaner.l 文件,该文件的框架和上一节中的 word-spliter.l 是完全一样的,只不过 token 的类别多了。

  1. %{
  2. #include "token.h"
  3. int cur_line_num = 1;
  4. void init_scanner();
  5. void lex_error(char* msg, int line);
  6. %}
  7.  
  8. /* Definitions, note: \042 is '"' */
  9. INTEGER ([0-9]+)
  10. UNTERM_STRING (\042[^\042\n]*)
  11. STRING (\042[^\042\n]*\042)
  12. IDENTIFIER ([_a-zA-Z][_a-zA-Z0-9]*)
  13. OPERATOR ([+*-/%=,;!<>(){}])
  14. SINGLE_COMMENT1 ("//"[^\n]*)
  15. SINGLE_COMMENT2 ("#"[^\n]*)
  16.  
  17. %%
  18.  
  19. [\n] { cur_line_num++; }
  20. [ \t\r\a]+ { /* ignore all spaces */ }
  21. {SINGLE_COMMENT1} { /* skip for single line comment */ }
  22. {SINGLE_COMMENT2} { /* skip for single line commnet */ }
  23.  
  24. {OPERATOR} { return yytext[0]; }
  25.  
  26. "<=" { return T_Le; }
  27. ">=" { return T_Ge; }
  28. "==" { return T_Eq; }
  29. "!=" { return T_Ne; }
  30. "&&" { return T_And; }
  31. "||" { return T_Or; }
  32. "void" { return T_Void; }
  33. "int" { return T_Int; }
  34. "while" { return T_While; }
  35. "if" { return T_If; }
  36. "else" { return T_Else; }
  37. "return" { return T_Return; }
  38. "break" { return T_Break; }
  39. "continue" { return T_Continue; }
  40. "print" { return T_Print; }
  41. "readint" { return T_ReadInt; }
  42.  
  43. {INTEGER} { return T_IntConstant; }
  44. {STRING} { return T_StringConstant; }
  45. {IDENTIFIER} { return T_Identifier; }
  46.  
  47. <<EOF>> { return 0; }
  48.  
  49. {UNTERM_STRING} { lex_error("Unterminated string constant", cur_line_num); }
  50. . { lex_error("Unrecognized character", cur_line_num); }
  51.  
  52. %%
  53.  
  54. int main(int argc, char* argv[]) {
  55. int token;
  56. init_scanner();
  57. while (token = yylex()) {
  58. print_token(token);
  59. puts(yytext);
  60. }
  61. return 0;
  62. }
  63.  
  64. void init_scanner() {
  65. printf("%-20s%s\n", "TOKEN-TYPE", "TOKEN-VALUE");
  66. printf("-------------------------------------------------\n");
  67. }
  68.  
  69. void lex_error(char* msg, int line) {
  70. printf("\nError at line %-3d: %s\n\n", line, msg);
  71. }
  72.  
  73. int yywrap(void) {
  74. return 1;
  75. }

上面这个文件中,需要注意的是,正则表达式中,用双引号括起来的字符串就是原始字符串,里面的特殊字符是不需要转义的,而双引号本身必须转义(必须用 \”\042 ),这是 flex 中不同于常规的正则表达式的一个特性。

除单字符运算符外的 token 的编号则在下面这个 token.h 文件,该文件中同时提供了一个 print_token 函数,可以根据 token 的编号打印其名称。

  1. #ifndef TOKEN_H
  2. #define TOKEN_H
  3.  
  4. typedef enum {
  5. T_Le = 256, T_Ge, T_Eq, T_Ne, T_And, T_Or, T_IntConstant,
  6. T_StringConstant, T_Identifier, T_Void, T_Int, T_While,
  7. T_If, T_Else, T_Return, T_Break, T_Continue, T_Print,
  8. T_ReadInt
  9. } TokenType;
  10.  
  11. static void print_token(int token) {
  12. static char* token_strs[] = {
  13. "T_Le", "T_Ge", "T_Eq", "T_Ne", "T_And", "T_Or", "T_IntConstant",
  14. "T_StringConstant", "T_Identifier", "T_Void", "T_Int", "T_While",
  15. "T_If", "T_Else", "T_Return", "T_Break", "T_Continue", "T_Print",
  16. "T_ReadInt"
  17. };
  18.  
  19. if (token < 256) {
  20. printf("%-20c", token);
  21. } else {
  22. printf("%-20s", token_strs[token-256]);
  23. }
  24. }
  25.  
  26. #endif

下面来编译一下这两个文件, makefile 文件为:

  1. out: scanner
  2.  
  3. scanner: lex.yy.c token.h
  4. gcc -o $@ $<
  5.  
  6. lex.yy.c: scanner.l
  7. flex $<

将以上 3 个文件保存在终端的当前目录,再输入 make ,编译后生成了 scanner 文件。

下面来测试一下这个词法分析器,将 samples.zip 文件下载并解压到 samples 目录,此文件包中有很多测试文件,我们先测试一下其中的一个文件,输入:

  1. $ ./scanner < samples/sample_6_function.c > out.txt

再打开 out.txt 文件看看,可以看出 sample_6_function.c 文件的所有 token 都被解析出来了:

  1. TOKEN-TYPE TOKEN-VALUE
  2. -------------------------------------------------
  3. T_Int int
  4. T_Identifier main
  5. ( (
  6. ) )
  7. ...

下面全部测试一下这些文件,在终端输入以下内容:

  1. for src in $(ls samples/*.c); do ./scanner < $src > $src.lex; done

再在终端输入: bash test.sh 。之后,查看一下 samples 目录下新生成的 ”.lex” 文件。可以看出所有源文件都被解析完成了。

TinyC 语言中只有两类词法错误,一种是未结束的字符串,即只有前面一个双引号的字符串,另外一种就是非法字符,如 ~ @ 等(双引号内部的除外),scanner.l 文件中可以识别出这两种词法错误,同时定位出错误所在的行,详见该文件的 Rules 段的最后两条 Rule 。

第 8 章完