yacc语法树系列(三)

922 阅读4分钟

这是我参与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动作可能使用伪变量11和2,...,指的是被一个规则rule右侧的部分返回的值,变量是从左到右读取的。因此,如果规则是下面这样:

        A       :       B  C  D   ;

比如说,2具有的值是由C返回的,那么2具有的值是由C返回的,那么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是如何工作的,敬请期待。