这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战
本文为译文,原文链接:dinosaur.compilertools.net/yacc/
接上文,继续下一节。
2: Actions动作
对于每一条语法规则,在每一次输入流识别到一条规则的时候,用户可能会关联上一些动作行为被执行。而且,预期词法分析器会返回tokens的值
一个行为是C语言的一个专用描述,像这种可以做输入和输出的,并且修改外部向量和变量的,叫子程序。一个行为动作被一个或多个声明指定,被花括号包围。例如:
A : '(' B ')'
{ hello( 1, "abc" ); }
XXX : YYY ZZZ
{ printf("a message\n");
flag = 25; }
上述这两种就是包含动作的语法规则。
为了帮助actions和解析器parser的容易交流,动作action声明被轻微修改了。美元符号"$"被用来作为一个yacc上下文的信号。
action动作很常见地给一些值设置伪变量"$$"来返回一个值,action动作可能使用伪变量2,...,指的是被一个规则rule右侧的部分返回的值,变量是从左到右读取的。因此,如果规则是下面这样:
A : B C D ;
比如说,3是由D返回的。
就像一个更具体的例子,考虑一个规则:
expr : '(' expr ')' ;
这条规则返回的值通常是expr在圆括号中返回的值。这可以被标记为:
expr : '(' expr ')' { $$ = $2 ; }
默认来说,一个规则的值是它第一个元素的值($1)。因此,下面语法规则的形式经常不需要有一个明确的动作。
在上面的例子中,所有的actions动作出现在rules规则末尾。有时候,在一个规则被完全解析之前获得控制是可取的。yacc允许一个动作被写在一个规则的中间或者结尾处。这个规则被假定为返回一个值,通过通常的机制,通过它右侧的动作actions可以访问这个值。反过来,它可能通过符号来访问值返回给左侧的值。因此,在这个规则:
A : B
{ $$ = 1; }
C
{ x = $2; y = $3; }
;
影响是设置x为1,设置y为C返回的值。
不终止一个rule规则的actions动作实际上是由yacc处理的,通过产生一个新的非终止符号名称name,然后一个新规则rule匹配上这个名称name为空字符串。这个内部动作是通过识别这个的额外的规则来激发的。yacc实际上把上面这个例子当成它已经被写成这样了:
$ACT : /* empty */
{ $$ = 1; }
;
A : B $ACT C
{ x = $2; y = $3; }
;
在很多应用中,输出并没有被动作actions直接做完;rather(准确地说),一个数据结构,例如一个解析树,是在内存中被构造出来的,在输出结果被生成之前对其应用转换。解析树是极其容易构造的,给予程序来构建和维护预期的树结构。例如,假设有一个C函数节点,
node( L, n1, n2 )
被写来让调用创建一个带有L标签的节点,还有n1和n2后代子节点,然后返回最新创建的节点的索引。然后解析树可以通过提供动作actions被建出来,比如说在规范中:
expr : expr '+' expr
{ $$ = node( '+', $1, $3 ); }
用户可以定义其他通过动作actions使用变量。声明和定义可以出现在声明部分,包裹在%{和%}中间。这些声明和定义有全局的范围,所以它们被熟知在动作声明和词法分析器。例如:
%{ int variable = 0; %}
这个可以被放在声明部分,让所有的动作都可以访问到变量。yacc解析器只使用命名names以"yy"开头;用户应该避免这些命名。
在这些例子中,所有的值都是integer:其他类型的值的讨论会被放在第十章。
3:Lexical Analysis词法分析
用户/使用者必须提供一个词法分析器来读取输入流并且传递tokens(如果期望,可以带上values)
给解析器。词法分析器是一个叫做yylex的整数类型值的函数。函数返回一个整数型,token号,代表读取到的token类别。如果有一个值value关联到那个token,它应该被分配给外部变量yylval。
解析器和语法分析器必须约定这些token号按顺序排列,因为它们之间发生交流。号码可能被yacc选中,或者被使用者选中。无论哪种情况,C语言的"#define"机制被用来允许词法分析器象征性地返回这些号码。比如说,假设一个命名为 DIGIT的token已经被定义在yacc规范文件中的声明部分。词法分析器的相关部分可能长这样:
yylex(){
extern int yylval;
int c;
. . .
c = getchar();
. . .
switch( c ) {
. . .
case '0':
case '1':
. . .
case '9':
yylval = c-'0';
return( DIGIT );
. . .
}
. . .
目的是返回一个DIGIT的token号码,并且一个跟digit数值型的值相等的值。提供词法分析器代码被放置在规范文件的程序章节中,标识符DIGIT将会被定义成token号码,关联上token DIGIT。
这种机制可以生成清晰,更容易修改的词法分析器;唯一的缺陷是需要避免使用任何的C语言或者parser解析器语法中的保留的和有意义的token命名;比如说,当词法分析器被编译的时候,if 或者while符号的命名的使用将会几乎确定导致严重的困难。token命名error被保留来错误捕获,不应该被天真地使用(看第七章)。
就像上面提高的,token号码可能被yacc或者使用者选中。在默认的场景下,号码是被yacc选中的。字面量字符的默认token号在本地字符集中 是数字值类型的字符。其他命名被指定的token numbers从257开始。
因为一些历史原因,结束标记必须有token number 0或者负数。这种token number不能被使用者重新定义;因此,所有词法分析器必须准备好返回0或者负数作为一个token number,当到达他们输入的结尾。
一个非常有用的构造词法分析器的工具是由Mike Lesk开发的Lex 程序。这些词法分析器被设计来跟yacc解析器一起密切工作的。为这些词法分析器的更规范使用了常规的表达式而不是语法规则。Lex可以被简单使用,来生成很复杂的词法分析器,但存在一些语言(例如FORTRAN)不适用任何理论框架,它们的词法分析器必须被手工制作。
未完待续...预告一下,下一节我们会讲解一下parser是如何工作的,敬请期待。