也可以
S → E ';'
S → S E ';'
或
S → E ';'
S → E ';' S
在你的第一个 sn-p 中,你说你“知道如何解析”,你的语法是模棱两可的。我假设您正在使用某种外部优先级/关联性声明对其进行解析,因为在没有指定如何构造解析树的情况下,除了简单识别之外,解析无法有意义地应用于文本。
我在这里提供的语法没有歧义,因此体现了列表的关联性。在许多情况下,列表的关联性是无关紧要的,因为期望的结果只是一个列表;在这种情况下,您选择上述哪个选项(从语法上来说)并不重要。
通常在使用 LR 解析器生成器时,我们会选择左关联的,也就是上面的第一个。这是因为右关联性要求将单个元素保留在解析器堆栈中,直到最终可以从后到前构造列表,当您到达末尾时。因此,解析一个长列表会占用大量的解析器堆栈;如果您的解析器生成器限制了堆栈的大小,那么这最终会成为一个问题。
从后到前的列表结构也可能会让新手感到困惑。一个常见的混淆(从关于 SO 的问题判断)来自以下“调试”代码(在 yacc/bison 语法中):(为简单起见,我实现 (E ';')* 而不是 (E ';')+;大多数时候,这就是你想要的无论如何。)
list: %empty
| element ';' list { printf("Adding %s\n", $1); }
这将导致列表中的元素从右到左打印出来,如果这是您期望代码执行的操作,这很好。但通常它会导致混乱,这对于调试代码有些适得其反。 (这只是为什么我总是建议使用内置在解析器生成器中的调试工具的原因之一——并且总是选择一个内置调试工具的解析器生成器——而不是试图用一组集合来创建解析器跟踪。特设print 声明。)
例如,如果解析器是即时计算器的一部分,那么从后到前的评估显然是一个巨大的错误。您想一次一个地评估然后丢弃表达式,从左到右和左关联是强制性的。
但情况并非总是如此。假设我们正在解析以构建 AST(或其他一些将导致代码生成的中间产品)。并且假设这里的元素是语句,而列表表示一个块(减去块分隔符,它将附加在一些外部生产中)。在块中的声明是块本地的并且范围从声明到块末尾的语言中,程序的语义强烈建议右关联性。考虑下面这个有点愚蠢的例子:
1 function example(i: integer)
2 var tmp: integer = i;
3 var i: integer = tmp - 1;
4 return tmp * i;
5 end
这里,tmp 的范围从语句 2 延伸到语句 4 的末尾。参数列表中 i 的范围从语句 1 延伸到语句 5,但在语句 3 中它被遮蔽通过声明另一个具有相同名称的变量,其范围从语句 3 延伸到语句 4 的末尾。
为了有意义地解析这种语言,我们需要在每次看到声明时创建一个新的子作用域,并将这个子作用域附加到在声明之后开始的程序部分。那会建议这样的语法:
block: %empty
| declaration ';' block { $3.set_scope(new Scope($1));
$3.prepend($1.initialiser()); }
| statement ';' block { $3.prepend($1); }
只是说清楚:有一些众所周知的转换成语
S → A B*
和
S → B* A
到上下文无关的语法。第一个是
S: A
| S B
第二个是
S: A
| B S
如果A 是B(换句话说,如果您想要S → B+,它表示与S → B B* 或S → B* B 相同的文本,您将使用S: B | S B 或S: B | B S。我上面没有这样做,因为它涉及重复 B 以及与之对应的任何操作,如果 B 不是单个符号,这很烦人。重复 B 没有任何问题(或创建一个中间非终端到表示它,如果它真的很复杂或有一个复杂的动作),但避免这个问题更简单。