【问题标题】:Can this seeming ambiguity be parsed in a LALR(1) parser (PLY)?可以在 LALR(1) 解析器 (PLY) 中解析这种表面上的歧义吗?
【发布时间】:2018-04-28 01:31:28
【问题描述】:

我在 PLY (Python Lexx Yacc) 中有一个大型语法,用于在解析方面有一些特殊挑战的语言。该语言允许两种调用的前导语法看起来几乎相同,直到调用非终端结束。这为减少/减少冲突提供了很多机会,因为沿途令牌的语义是不同的,但可以从相同的终端令牌构建。我提取了以下语法的简单前后版本,我会稍微解释一下。

最初,表达式是一种典型的“分层语法”,将调用和文字等进行初级,然后通过一元进行初级,然后将二进制变为一般表达式。问题是带有两个参数的Call_exprIter_expr 的版本冲突,该版本以'/' 之前的两个ID 开头。冲突在调用中的第一个参数之后的逗号上,因为最初,Expr -> ... -> Primary_expr -> Name_expr -> Id 是允许的。解析器可以将Id 减少到Expr 以匹配Call_expr,或者让它匹配Iter_expr。展望逗号并没有帮助它做出决定。如果调用的第一个参数只是一个标识符(如变量),这是合法的歧义。考虑输入id > id ( id , id ...

我的方法是制作一种可能只是Id的表达式。我通过所有表达式添加了生产链,以赋予它们“_nn”版本--“不是名称”。然后我可以为Call_expr 定义产生式,在第一个参数中使用任何语法,使其不仅仅是一个名称(例如运算符、调用等),以将其简化为BinOp_expr_nn也是 允许仅以Id 作为第一个参数的调用产生。这应该会说服解析器只是移动,直到它可以解析 Iter_exprCall_expr(或者至少知道它在哪条路径上。)

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

所以,我的问题是:是否有人可以阐明如何让相同的标记表示不同的语义(即 expr 与 id)仍然可以像 PLY 一样用 LALR(1) 解析?除此之外,还有什么有用的技巧可以帮助解决问题吗?这可以消除歧义吗?

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

原文语法:

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 中的第一个参数的歧义:

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

【问题讨论】:

  • 我只是将 Iter_Expr 和 Call_Expr 折叠成一个语法,然后通过遍历 AST 来梳理它是什么。如果你知道任何 Lisp,那应该是显而易见的。也就是说,在 Call_Expr 语法中允许那些 Id : Id / Expr 形式。然后遍历 Call_Expr 节点以确定它们是否实际上是有效的 Iter_exprs。这将过度生成:允许对 Id 的无效使用:Id / Expr;您可以检查并诊断。
  • @rici:很抱歉出现错误,但您的解释最正确。 '>' 应该在终端列表中;它将某些类型的呼叫与我的玩具版本中未包含的其他呼叫区分开来。所有这些调用都可以链接为“foo > bar(baz / a + baz) > qux(b, c, d, e)”,并且“>”的每个 lhs 都是一个 primary_expr。 '/' 是另一个语法分隔符,而不是运算符。很抱歉造成混淆:其中大部分来自简化,断章取意,我敢肯定这看起来很奇怪。
  • 我仍然认为你想要BinOp_expr -> Unary_expr | BinOp_expr '+' Unary_expr(这样+ 的关联性较低;正如所写,它是模棱两可的)和Unary_expr -> Primary_expr | '^' Unary_expr 这样^ a + b 意味着(^a) + b 而不是@ 987654349@)。 (或者可能是Unary_expr -> Primary_expr | '^' Primary_expr,如果不允许使用^^a。)
  • 您可能还需要Primary_expr -> '(' Expr ')' 以允许使用括号。
  • 是的,我正在考虑这些。目前,我正在使用优先级来解决所有这些问题,但会解决歧义。感谢您和@Kaz 的精彩建议。非常感谢,我现在应该上路了!

标签: parsing yacc ply lalr


【解决方案1】:

尽管您的帖子有标题,但您的语法并不含糊。由于您提到的原因,它根本不是 LR(1):输入

A ( B , 

可以是Call_exprIter_expr 的开头。在第一种情况下,B 必须缩减为Expr,然后再缩减为Args;在第二种情况下,它不能被减少,因为当右边的id '(' id ',' id ':' id '/' Expr ')'被减少时,它仍然需要是一个id。不能仅通过查看单个前瞻标记()来做出决定,因此存在移位减少冲突。

这个冲突最多可以通过两个额外的前瞻标记来解决,因为只有在 , 后面紧跟着一个 id 然后是一个 :。这样就形成了语法 LALR(3)。不幸的是,Ply 不生成 LALR(3) 解析器(yacc/bison 也不生成),但有一些替代方案。

1。使用不同的解析算法

由于语法是明确的,因此可以使用 GLR 解析器毫无问题地解析(并且无需任何修改)。 Ply 也不产生 GLR 解析器,但 Bison 可以。这可能对你没有多大用处,但我想我应该提一下,以防你没有被 Python 锁定。

2。使用允许一些无效输入的语法,并通过语义分析将其丢弃

这几乎可以肯定是最简单的解决方案,也是我通常会推荐的。如果把Iter_expr的定义改成:

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

那么它仍然会识别每个有效输入(因为idid , id 都是Args 的有效实例)。这消除了班次减少冲突;实际上,它让解析器避免在遇到 )(表示Call_expr)或 :(表示@987654336)之前做出决定@)。 (Iter_expr 的第一个替代方案没有问题,因为移动 / 而不是减少 id 的决定不需要额外的前瞻。)

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

3。使用词法技巧

弥补前瞻信息缺失的一种方法是在词法分析器中收集信息,方法是将必要的前瞻信息收集到缓冲区中,并且仅在其句法类别已知时才输出词位。在这种情况下,词法分析器可以查找序列'(' id ',' id ':' 并标记第一个id,以便它只能在Iter_expr 中使用。在这种情况下,语法的唯一变化是:

Iter_expr : id '(' id '/' Expr ')'
          | id '(' id ':' id '/' Expr ')'
          | id '(' iter_id ',' id ':' id '/' Expr ')'

虽然这在这种特殊情况下可以正常工作,但它不是很容易维护。特别是,它依赖于能够定义一个可以在词法分析器中实现的简单且明确的模式。由于该模式是语法的简化,因此很可能未来的一些句法添加将创建一个也匹配相同模式的语法。 (这被称为词法“hack”是有原因的。)

4。求一个 LALR(1) 文法

如前所述,此语法为LALR(3)。但是没有LALR(3) 语言这样的东西。或者更准确地说,如果一种语言有LALR(k) 语法,那么它也有LALR(1) 语法,并且该语法可以从LALR(k) 语法机械地产生。此外,给定单符号前瞻文法的解析,可以重建原始文法的解析树。

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

然而,这并不难。例如,这里是您的语法的一个稍微简化的版本,删除了多余的单位产生式并修复了运算符优先级(可能不正确,因为我不知道您的目标是什么语义)。

名称以N 结尾的产生式不产生ID。对于它们中的每一个,都有一个对应的产生式,没有添加IDN。完成后,有必要重写Args 以使用ExprN 产生式,并允许以一个或两个IDs 开头的各种参数列表。 Chain 的产生只是为了避免一些重复。

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

我没有像我想要的那样测试这个,但我认为它产生了正确的语言。

【讨论】:

  • 感谢您如此周到。我喜欢更明确的语法的想法,但它确实导致许多减少/减少冲突,需要仔细排序的规则。我什至不确定我能不能让它全部工作(考虑“a + b + c”,解析为“Expr '+' UnaryN . '+' 'c'”。如果我们应用 ExprN -> UnaryN,我们会转移in 并执行 "b+c" 成为 ExprN。现在,没有办法减少 Expr '+' Unary)。鉴于语法的复杂性以及需要如此仔细地排序和调整,您觉得是走这条路更好,还是只是打开语法并通过检查 AST/语义来处理事情?
  • @jspencer:我将解决方案按吸引力的粗略顺序排列,并在第二个选项中明确表示这是我推荐的选项(除非您可以使用 GLR 生成器)。
  • @jspencer:不过,我确实测试了我提供的语法。它将x(a+b+c) 解析为('Call', ['x', '(', ('Args', [('ExprN', [('ExprN', ['a', '+', 'b']), '+', 'c'])]), ')']),这是正确的,假设您的+ 运算符是左关联的。 (注意:我使用的语法树构造函数没有显示单位缩减。)
  • 也许更好的例子是“a + b > c() + d”(应该是“a + (b > c()) + d。”我可能还有其他并发症,因为完整的语法,但我遇到了很多这样的情况,在不同的情况下任何一个选择都是正确的.我仍然没有牢牢掌握如何防止这些冲突,而是迫使解析器沿着(总是)正确的路径. 至少,不是完整的语法(约 160 条规则)。更难!:)
  • @jspencer:那个也很好用。我解析了x(a+b>c()+d)(因为您的语法需要调用或迭代作为顶级形式),结果解析实际上是x((a+(b>c()))+d)。是什么让你认为这是错误的?我的 Ply 程序很简单;它主要是我已经粘贴的语法。但如果你真的无法重现它,我可以把它放在某个地方。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多