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

时间:2018-04-28 01:31:29

标签: parsing yacc ply lalr

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

最初,表达是典型的"分层语法,"接受调用和文字等主要,然后主要通过一元,然后二进制到一般表达。问题是Call_expr有两个参数与Iter_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 vs 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

1 个答案:

答案 0 :(得分:3)

尽管你的帖子标题,但你的语法并不含糊。它简直不是LR(1),因为你提到:输入

A ( B , 

可以是Call_exprIter_expr的开头。在第一种情况下,B必须缩减为Expr,然后归结为Args;在第二种情况下,它不能减少,因为当右侧id减少时它仍然需要id '(' id ',' id ':' id '/' Expr ')'。不能简单地通过查看单个先行标记()来做出决定,因此存在一个shift-reduce冲突。

这个冲突最多可以通过两个额外的前瞻标记来解决,因为如果后面跟着恰好是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)或(表示Iter_expr)。 (Iter_expr的第一个替代方案没有问题,因为决定移动 / 而不是减少id不需要额外的预测。)

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

3。使用词汇黑客

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

Iter_expr

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

4。找一个LALR(1)语法

如上所述,该语法为Iter_expr : id '(' id '/' Expr ')' | id '(' id ':' id '/' Expr ')' | id '(' iter_id ',' id ':' id '/' Expr ')' 。但是没有LALR(3) 语言这样的东西。或者更确切地说,如果一种语言具有LALR(3)语法,那么它也具有LALR(k)语法,并且该语法可以从LALR(1)语法机械地生成。此外,给定一个符号前瞻性语法的解析,可以重建原始语法的解析树。

因为机械生成的语法非常大,所以这个程序不是很吸引人,而且我不知道该算法的实现。相反,最常见的是尝试重写语法的一部分,正如您在原始问题中尝试的那样。当然,这可以做到,但最终结果并不完全直观。

但是,并不困难。例如,这里是一个略微简化的语法版本,删除了冗余单元产品,并在运算符优先级中进行了一些修复(可能不正确,因为我不知道你的目标是什么语义)

名称以LALR(k)结尾的作品不会产生N。对于他们中的每一个,都有一个相应的作品而没有添加ID的{​​{1}}。完成后,有必要重写N以使用ID生成,以及允许以一个或两个Args开头的各种参数列表。 ExprN制作只是为了避免重复。

ID

我没有像我想要的那样对此进行测试,但我认为它会产生正确的语言。