制作 crowbar 要点记录
- 奇怪的计算器
- lex
- yacc
- 生成执行文件
- 执行流程以及生成的文件
- 冲突的含义
1 2 3
| 搭建环境 UNIX 标准环境下已预装 yacc、lex Windows 使用免费替代品 bison、flex
|
编程语言的语法处理过程:词法分析 =)语法分析 =)语义分析 =)生成代码
- 词法分析,分割 token,由词法分析器(lexical analyzer, lexer, scanner)执行,如
lex
根据此法规则生成词法分析器
- 语法分析,构建抽象语法树 abstract stntax tree, AST,由解析器执行,如
yacc
(Yet Another Compiler Compiler)会根据语法规则生成解析器程序
- 语义分析,上一个步骤不包含
数据类型
等语义信息,因此这里会检查是否有语法正确但是逻辑
错误的问题
- 生成代码
奇怪的计算器
现在借助开发奇怪计算器 mycalc 的例子来熟悉工具。
lex
lex 是自动生成词法分析器
的工具,通过输入扩展名为 .l 的文件,输出词法分析器的 C 代码。
词法分析会把字符串分割为记号,因此首先定义 mycalc 所用到的记号,使用正则表达式定义记号。
1
| 运算符,+、-、*、/;整数;实数;换行符,输入完算式即刻换行就执行程序,所以换行符也是记号
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| /* 文件 mycalc.l */ %{ #include <stdio.h> #include "y.tab.h"
int yywrap(void) { return 1; } %} %% /* 本行之前叫`定义区块`,用来定义初始状态或者为正则表达式命名 */ /* 这里的定义区块里放了 C 代码,其中如果缺失 yywrap 函数就必须手动链接 lex 的库文件,在不同环境下编译时会出现麻烦 */ "+" return ADD; "-" return SUB; "*" return MUL; "/" return DIV; "\n" return CR; ([1-9][0-9]*)|0|([0-9]+\.[0-9]+) { double temp; sscanf(yytext, "%lf", &temp); yylval.double_value = temp; return DOUBLE_LITERAL; } [ \t] ; . { fprintf(stderr, "lexical error. \n"); exit(1); } /* 规则区块,一个正则后跟若干空格,再跟 C 代码(动作 action),返回特征符 */ %% /* 本行以后叫`用户代码区块`,可编写任意 C 代码 */
|
yacc
yacc 是自动生成语法分析器
的工具,通过输入扩展名为 .y 的文件,输出语法分析器的 C 代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| /* 文件 mycalc.y */ %{ #include <stdio.h> #include <stdlib.h> #define YYDEBUG 1 // 设置非 0 值开启 Debug 模式,可以看到运行时语法分析状态 %} %union { int int_value; double double_value; // 声明`非终结符`类型,此类型的对立面是`终结符` } %token <double_value> DOUBLE_LITERAL /* 值为 DOUBLE_LITERAL 的记号,类型为 <double_value>,double_value 来自上方 %union 集合的一个成员名 */ %token ADD SUB MUL DIV CR /* 记号类型 */ %type <double_value> expression term primary_expression /* 声明非终结符类型 */ %% /* 本行以后的区块叫`规则区块`,由语法规则和 C 编写的相应动作两部分组成 */ /* 使用 BNF 规范编写语法规则,巴科斯范式,巴库斯-瑙尔范式,Backus Normal Form */ line_list : line | line_list line ; line : expression CR { printf(">>%lf\n", $1); } | error CR /* 简单的错误恢复机制 */ { yyclearin; /* 丢弃预读的记号 */ yyerrok; /* 通知 yacc 程序已经从错误状态恢复了 */ } ; expression : term | expression ADD term { $$ = $1 + $3; // 记号值,$1 是 expression,$3 是 term,$2 不存在记号值,因为它是加法运算符 +,计算结果会赋给 $$ 保存在栈中 } | expression SUB term { $$ = $1 - $3; } ; term : primary_expression | term MUL primary_expression { $$ = $1 * $3; } | term DIV primary_expression { $$ = $1 / $3; } ; primary_expression : DOUBLE_LITERAL ; %% int yyerror(char const *str) { extern char *yytext; fprintf(stderr, "parser error near %s\n", yytext); return 0; }
int main(void) { extern int yyparse(void); extern FILE *yyin;
yyin = stdin; if (yyparse()) { fprintf(stderr, "Error ! Error ! Error !\n"); exit(1); } }
|
yacc 的运作方式:移进 shift;归约 reduce。记号进入并堆积叫移进,记号和记号之间触发了某个规则并进行置换叫归约。
生成执行文件
1 2 3 4
| % yacc -dv mycalc.y % lex mycalc.l % cc -o mycalc y.tab.c lex.yy.c -std=c89 // 使用 C89 https://stackoverflow.com/questions/27220759/linker-error-yacc-on-mac
|
执行流程以及生成的文件
冲突的含义
yacc 运行时会发生规约 / 规约(reduce/reduce)
冲突和移进 / 规约(shift/reduce)
冲突。我们查看y.output
文件了解冲突情况,yacc 运行时附带-v
参数将生成 y.output 文件,即生成执行文件步骤的yacc -dv mycalc.y