郁郁青青 长过千寻

制作 crowbar 要点记录


    1. 奇怪的计算器
      1. lex
      2. yacc
      3. 生成执行文件
        1. 执行流程以及生成的文件
        2. 冲突的含义

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

执行流程以及生成的文件

  • mycalc.y
    • yacc
      • y.tab.c
        • + lex.yy.c =)mycalc 执行文件
      • y.tab.h
  • mycalc.l
    • lex
      • lex.yy.c
        • + y.tab.c =)mycalc 执行文件

冲突的含义

yacc 运行时会发生规约 / 规约(reduce/reduce)冲突和移进 / 规约(shift/reduce)冲突。我们查看y.output文件了解冲突情况,yacc 运行时附带-v参数将生成 y.output 文件,即生成执行文件步骤的yacc -dv mycalc.y

1

页阅读量:  ・  站访问量:  ・  站访客数: