关于解析:在LALR(1)解析器(PLY)中是否可以解析这种看似含糊不清的问题?

Can this seeming ambiguity be parsed in a LALR(1) parser (PLY)?

我在ply(python-lexx-yacc)中有一个大型的ish语法,用于解析过程中有一些特殊挑战的语言。语言允许两种调用的前导语法看起来几乎相同,直到调用非终端结束。这为减少/减少冲突提供了很多机会,因为沿途令牌的语义不同,但可以用相同的终端令牌构建。我已经提取了下面语法的简单前后版本,我将对此做一点解释。

最初,表达式是一种典型的"分层语法",将调用和文本等转换为主表达式,然后转换为一元表达式,再转换为二元表达式。问题是,有两个参数的Call_expr与以'/'前面的两个id开头的Iter_expr版本冲突。冲突发生在调用中第一个参数后的逗号上,因为最初允许使用Expr -> ... -> Primary_expr -> Name_expr -> Id。解析器可以将Id减少到Expr以匹配Call_expr,或者让它与Iter_expr匹配。展望逗号并不能帮助它做出决定。如果调用的第一个参数只是一个标识符(如变量),这是合法的歧义。考虑输入id > id ( id , id ...

我的方法是做一种表达,不能仅仅是一个Id。我通过所有的表达式添加了生产链,给它们提供"_nn"版本——"不是名称"。然后我可以为Call_expr定义生产,它在第一个参数中使用任何语法,使其不仅仅是名称(如运算符、调用等),以将其简化为BinOp_expr_nn,还允许具有作为第一个论点。这应该能说服解析器切换,直到它能够解析Iter_exprCall_expr(或者至少知道它在哪个路径上)。

你可能已经猜到了,这把一切都搞砸了。对表达式链的修改也对Primary_expr进行了修改,我仍然需要允许将其减少到Id。但现在,这是一种减少/减少冲突——每个Primary_expr都可以留在那里或继续到Unary_expr去。我可以命令他们做出选择(这可能有效),但我希望我最终会追逐其他人。

所以,我的问题是:有没有一种技术可以让人清楚地表达,如何允许相同的令牌代表不同的语义(即expr和id),这些语义仍然可以用lalr(1)样的ply进行解析?除此之外,还有什么有用的帮助解决问题的黑客吗?这能消除歧义吗?

1
2
3
terminals:  '+' '^' ',' '>' '(' ')' '/' ':' 'id' 'literal'
   (i.e. punctuation (besides '->' and '|', initial-lower-case words)
non-terminals:  initial-Upper-case words

原始语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
S'-> S
S -> Call_expr
   | Iter_expr
Expr -> BinOp_expr
BinOp_expr -> Unary_expr
BinOp_expr -> BinOp_expr '+' BinOp_expr
Unary_expr -> Primary_expr
   | '^' BinOp_expr
Primary_expr -> Name_expr
   | Call_expr
   | Iter_expr
   | Literal_expr
Name_expr -> Id
Args -> Expr
   | Args ',' Expr
Call_expr -> Primary_expr '>' Id '(' ')'
   | Primary_expr '>' Id '(' Args ')'
Iter_expr -> Primary_expr '>' Id '(' Id '/' Expr ')'
   | Primary_expr '>' Id '(' Id ':' Id '/' Expr ')'
   | Primary_expr '>' Id '(' Id ',' Id ':' Id '/' Expr ')'
Literal_expr -> literal
Id -> id

我试图消除Call_expr中第一个论点的歧义:

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
S'-> S
S -> Call_expr
   | Iter_expr
Expr -> BinOp_expr_nn
   | BinOp_expr
BinOp_expr -> BinOp_expr_nn
   | Unary_expr
BinOp_expr_nn -> Unary_expr_nn
   | BinOp_expr '+' BinOp_expr
Unary_expr -> Primary_expr
   | Unary_expr_nn
Unary_expr_nn -> Primary_expr_nn
   | '^' BinOp_expr
Primary_expr -> Primary_expr_nn
   | Name_expr
Primary_expr_nn -> Call_expr
   | Iter_expr
   | Literal_expr
Name_expr -> Id
Args -> Expr
   | Args ',' Expr
Call_expr -> Primary_expr '>' Id '(' ')'
   | Primary_expr '>' Id '(' Expr ')'
   | Primary_expr '>' Id '(' Id , Args ')'
   | Primary_expr '>' Id '(' BinOp_expr_nn , Args ')'
Iter_expr -> Primary_expr '>' Id '(' Id '/' Expr ')'
   | Primary_expr '>' Id '(' Id ':' Id '/' Expr ')'
   | Primary_expr '>' Id '(' Id ',' Id ':' Id '/' Expr ')'
Literal_expr -> literal
Id -> id


尽管你的文章有标题,但你的语法并不含糊。它不是lr(1),因为您提到的原因:输入好的。

1
A ( B ,

可以是Call_exprIter_expr的开头。在第一种情况下,B必须先降为Expr,然后降为Args;在第二种情况下,不能降为id '(' id ',' id ':' id '/' Expr ')',因为当右手边id '(' id ',' id ':' id '/' Expr ')'降为id时,它仍需为id。不能简单地通过查看单个先行令牌()来作出决定,因此存在一个移位减少冲突。好的。

这种冲突最多可以用两个附加的先行令牌来解决,因为只有在后面紧跟id:时,移位才是有效的操作。这就使得语法变得很简单(3)。不幸的是,ply没有生成lalr(3)解析器(yacc/bison也没有),但是有一些替代方法。好的。1。使用其他分析算法

因为语法是明确的,所以可以使用GLR解析器进行解析,而不会出现任何问题(也不会进行任何修改)。PLY也不生产GLR解析器,但是Bison可以。这对您可能没什么用处,但我想我应该提一下,以防您没有被锁定在Python的使用中。好的。2。使用允许一些无效输入的语法,并通过语义分析丢弃它们

这几乎肯定是最简单的解决方案,我通常会推荐它。如果您将Iter_expr的定义更改为:好的。

1
2
Iter_expr : id '(' id '/' Expr ')'
          | id '(' Args ':' id '/' Expr ')'

然后它仍然会识别每个有效输入(因为idid , id都是Args的有效实例)。这消除了移位-减少冲突;实际上,它使解析器避免做出决定,直到遇到)(表示Call_expr:(表示Iter_expr为止)。(对于Iter_expr的第一个替代方案没有问题,因为决定改变/而不是减少id不需要额外的前瞻性。)好的。

当然,Iter_expr的第二个产品将识别出许多不是有效迭代表达式的东西:超过2个项目的列表,以及包含比单个id更复杂的表达式的列表。但是,这些输入根本不是有效的程序,因此在Iter_expr的操作中可以简单地拒绝它们。识别有效迭代的精确代码将取决于您如何表示AST,但并不复杂:只需检查以确保Args的长度是一个或两个,并且列表中的每个项目都只是一个id。好的。三。使用词汇黑客

弥补lookahead信息不足的一种方法是在lexer中收集它,方法是将必要的lookahead收集到缓冲区中,并且仅在其语法类别已知时输出lexem。在这种情况下,lexer可以查找序列'(' id ',' id ':',并标记第一个id,以便它只能用于Iter_expr。在这种情况下,对语法的唯一更改是:好的。

1
2
3
Iter_expr : id '(' id '/' Expr ')'
          | id '(' id ':' id '/' Expr ')'
          | id '(' iter_id ',' id ':' id '/' Expr ')'

虽然在这种特殊情况下这会很好地工作,但它的维护性不是很强。特别是,它依赖于能够定义一个简单而明确的模式,该模式可以在lexer中实现。因为这种模式是对语法的简化,所以很有可能将来的一些句法添加会创建一个同样符合相同模式的语法。(这被称为词汇"hack"是有原因的。)好的。4。找到一个LALR(1)语法

如前所述,这种语法是LALR(3)。但是,没有一种语言是LALR(3)语言。或者更准确地说,如果一种语言有一个LALR(k)语法,那么它也有一个LALR(1)语法,并且该语法可以从LALR(k)语法机械地生成。此外,对于一个符号先行语法的解析,可以为原始语法重建解析树。好的。

因为机械生成的语法相当大,所以这个过程不是很有吸引力,我不知道算法的实现。相反,最常见的方法是尝试只重写语法的一部分,就像在最初的问题中所做的那样。当然,这是可以做到的,但最终的结果并不是完全直观的。好的。

不过,这并不难。例如,这里是您的语法的一个稍微简化的版本,删除了多余的单元生成,并在运算符优先级中修复了几个(可能不正确,因为我不知道您要寻找什么语义)。好的。

名称以N结尾的产品不生产id。对于每种产品,都有相应的产品,没有添加idN。一旦这样做了,就有必要重写Args来使用ExprN的生产,并允许从一个或两个id开始的各种参数列表。Chain的生产只是为了避免一些重复。好的。

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
Start   : Call
        | Iter
Expr    : ID
        | ExprN
ExprN   : UnaryN
        | Expr '+' Unary
Unary   : ID
        | UnaryN
UnaryN  : ChainN
        | '^' Chain
Chain   : ID
        | ChainN
ChainN  : PrimaryN
        | Chain '>' CallIter
PrimaryN: LITERAL
        | Call
        | Iter
        | '(' Expr ')'
Call    : ID '(' ')'
        | ID '(' ID ')'
        | ID '(' ID ',' ID ')'
        | ID '(' Args ')'
Iter    : ID '(' ID '/' Expr ')'
        | ID '(' ID ':' ID '/' Expr ')'
        | ID '(' ID ',' ID ':' ID '/' Expr ')'
Args    : ExprN ExprList
        | ID ',' ExprN ExprList
        | ID ',' ID ',' Expr ExprList
ExprList:
        | ExprList ',' Expr

我并没有像我想的那样对它进行测试,但我认为它产生了正确的语言。好的。好啊。