【问题标题】:Is C#'s lambda expression grammar LALR(1)?C# 的 lambda 表达式语法是 LALR(1) 吗?
【发布时间】:2013-05-31 11:48:25
【问题描述】:

标题中简洁地给出了我想问的问题。让我举一个有问题的语法的例子:

identifier_list
    : identifier
    | identifier_list identifier;

lambda_arguments
    : '(' identifier_list ')'
    | identifier;

lambda
    : lambda_arguments '=>' expression

然后我们添加正常的 C 表达式语法 - 特别是,

primary_expression
    : '(' expression ')'
    | identifier
    | lambda;

真正的问题是,这个语法 LALR(1) 是否可解析,即能够被自动解析器生成器解析?还是需要手动或 GLR 解析器?请注意,我希望具体了解本小节,而不是上下文相关的关键字或任何其他部分。

我现在在想的是,如果解析器看到'(' identifier ')',这有两个有效的解析,所以如果解析器看到identifier,向前看')',它将无法决定哪个解析树下去。不过,这可能只是一个移位/减少冲突,我可以通过分配一些任意优先级来消除它(可能有利于'(' identifier ')')。

编辑:实际上,我正在考虑 stealing 使用语法的这个小节来实现新语言中的类似功能。我已经有语法形式类似于 JavaScript 的匿名函数,但我的 gunea pigs squeaks 反馈抱怨它们对于许多用途来说过于冗长,并指出 C# lambda 表达式是更理想的解决方案。我担心此解决方案可能导致歧义。所以,真的,我只对那个小节感兴趣。泛型和演员表等其他东西对我来说不是问题。

我以前的语法版本是可机械解析的,我不想失去这个属性,而我之前使用机械生成器的经验告诉我,最好在这里检查而不是自己尝试。对于我的手动解析器,我当然可以简单地使用特殊情况 '(' identifier 来比正常情况更进一步。

【问题讨论】:

  • @EricLippert 或许可以回答这个问题。
  • 这是一个@JonSkeet 问题
  • 虽然 Eric 和 Jon 真的很聪明并且知道很多(有时是内部的)东西,但似乎这个语法问题可以由没有“内部知识”的人来回答(阅读:远高于我的)对语法和解析器的理解..
  • LALR(1)、手辊和 GLR 不是唯一的选择。只要语法是明确的,只要语法不包含间接或隐藏的左递归,ANTLR 4 就能够生成有效的解析器。

标签: c# parsing lalr


【解决方案1】:

首先,解析器理论一直是我的弱点之一。我主要研究语义分析器。

其次,我曾经使用过的所有 C# 解析器都是手动生成的递归下降解析器。我的一位在解析器理论方面拥有深厚背景的前同事确实构建了自己的解析器生成器,并成功地将 C# 语法输入其中,但我不知道这样做会带来什么样的骇人听闻的黑客攻击。

所以我在这里要说的是带着适当的怀疑态度来回答这个问题。


正如你所注意到的,lambda 有点令人烦恼,因为你必须小心那个带括号的表达式——它可能是一个带括号的表达式、一个强制转换运算符或一个 lambda 参数列表,而 lambda 参数列表可能是几个不同的形式。但考虑到所有因素,在 C# 3.0 中添加 lambda 在语法上相对容易;破解解析器并不太难——语义分析对 lambdas 来说是个难题。

就前瞻而言,C# 语法中真正令人头疼的问题是 泛型 和 casts。

泛型是在 C# 2 中添加的,此前该语言已经具有 >>、> 和 < 运算符,当您将泛型混入其中时,所有这些都会导致奇怪的问题。

经典问题当然是A ( B < C, D > ( E ) ) 方法的调用A 有两个参数:B < C 和D > (E) 还是一个B<C,D>( E )?

消除歧义的规则是:

如果可以将标记序列解析为以类型参数列表结尾的简单名称、成员访问或指针成员访问,则检查紧跟在结束 > 标记之后的标记。如果它是( ) ] : ; , . ? == != 之一,则类型参数列表作为简单名称、成员访问或指针成员访问的一部分保留,并且丢弃标记序列的任何其他可能解析。否则,即使没有其他可能的标记序列解析,类型参数列表也不被视为简单名称、成员访问或指针成员访问的一部分。

语法的第二个问题可以追溯到 C# 1.0,那就是强制转换运算符。问题是(x)-y 可能意味着“将-y 转换为类型x”或者它可能意味着从x 中减去y。这里的规则是:

仅当以下至少一项为真时,括号中的一个或多个标记的序列才被视为强制转换表达式的开始:

标记序列对于类型来说是正确的语法,但对于表达式来说是不正确的。

记号序列对于一个类型来说是正确的语法,紧跟在右括号后面的记号是记号“~”、记号“!”、记号“(”、标识符、文字或任何关键字除了as 和is。

消除这两种情况的规则在理论上涉及潜在的大前瞻,但实际上您很少需要将解析器备份得很远。

【讨论】:

  • 让我想起了我自己的问题……stackoverflow.com/questions/14412533
  • 感谢您对一般问题的洞察力,但我的问题确实非常具体。不过,我很好奇您发现 lambda 分析的哪一部分困难,因为我发现它们真的很容易实现。
  • @DeadMG:您可能有一个规范可以使用,并且不会每天都在变化。最困难的部分是改造现有的匿名方法分析来处理推测性的 lambda 绑定;该代码充满了全局可变状态,这使得进行推测变得困难。获得可接受的性能也是一项艰巨的工作,设计一种在典型场景中呈现良好错误消息的算法也是如此。
【解决方案2】:

用 C# 风格的 lambda 增强的表达式语法不是 LALR(1),但它可能是 LALR(2)。因此,有可能(尽管不一定是微不足道的)产生等效的 LALR(1) 语法:请参阅下面的编辑。

您将在输入上遇到 reduce/reduce 冲突:

( id )

因为id 可以简化为identifier_list 或expression(间接地,在第二种情况下),并且解析器无法根据一个前瞻令牌())判断哪个是正确的。

它可以基于两个前瞻标记来判断,因为只有当第二个下一个标记是 => 时,identifier_list 减少才有可能,并且只要 => 不是您的语言中的运算符,expression如果第二个下一个令牌是=>,则无法减少。所以我认为可能是 LALR(2),虽然我不能肯定地说。

存在多个标识符的情况没有问题,因为在

( id1 id2 )

id1 id2 不能简化为表达式(在大多数表达式语言中;当然,您的可能会有所不同)。如果 `=>' 不是有效的运算符,则单个未加括号的标识符后面紧跟 => 的情况也没有问题。

编辑

我在原始答案中没有提到没有 LALR(2) 语言之类的东西。 LALR(2) 语法识别的语言也被某些 LALR(1) 语法识别。事实上,这个断言有一个建设性的证明,它允许机械地创建这样的 LALR(1) 语法,以及恢复原始分析树的过程。

在这种情况下,生成 LALR(1) 语法更加简单,因为如上所述,只有一个产生式需要额外的前瞻。解决方案是将减少延迟一个令牌。换句话说,在原始语法中包含类似这样的内容:

primary:           '(' expression ')'
lambda_parameters: '(' id_list ')'

id_list 和 expression 都派生出终端 ID。除了ID,这两个非终结符的推导是不相交的,所以我们可以解决这个问题:

primary:           '(' expression_not_id ')'
       |           '(' ID ')'


lambda_parameters: '(' id_list_not_id ')'
                 | '(' ID ')'

剩下的只是将expression和id_list的产生式分开,以分离出ID的情况,结果并不难。下面是一个简化的例子,可以很容易地扩展;它仅限于加法、乘法和函数应用(我包括在内以证明两个逗号分隔的列表不是问题):

%token ID LITERAL RIGHT_ARROW
%start expr
%%
primary: primary_not_id | ID ;
term:    term_not_id    | ID ;
sum:     sum_not_id     | ID ;
expr:    expr_not_id    | ID ;

expr_list: expr         | expr_list ',' expr ;
arguments: '(' ')'      | '(' expr_list ')' ;

ids: ID ',' ID          | ids ',' ID ;
parameters: '(' ID ')'  | '(' ids ')' ;

primary_not_id: LITERAL
              | '(' expr_not_id ')'
              | '(' ID ')'
              | primary arguments
              ;

term_not_id: primary_not_id
           | term '*' primary
           ;

sum_not_id: term_not_id
          | sum '+' term
          ;

expr_not_id: sum_not_id
           | parameters RIGHT_ARROW expr
           ;

注意:OP 中的语法生成具有多个参数的 lambda,作为不以逗号分隔的标识符序列:(a b) => a + b。我认为实际意图是使用逗号:(a, b) => a + b,这就是我在上面的语法中所做的。如果您的语言像 C 系列那样具有逗号运算符,则差异很重要,因为在这种情况下,表达式可能是 '(' expression_list ')',这与 lambda 参数列表冲突。幼稚的实现会导致 expression_list 中的第一个 expression 发生减少/减少冲突,这无法通过有限前瞻来解决,因为 expression_list 可以任意长。

不过,对于这种情况也有一个解决方案:它包括将 id_list 与 expression_list 分开,如下所示:

id_list:         ID
       |         id_list ',' ID
       ;
expression_list_not_id_list: expression_not_id
                           | id_list ',' expression_not_id
                           | expression_list_not_id_list ',' expression
                           ;
expression_list: expression_list_not_id_list
               | id_list
               ;

不过,我没有做完整的语法,因为我不知道目标语言需要什么。

【讨论】:

  • 呃,是的,我本来就是想拥有(a, b)
【解决方案3】:

是的,这种情况是直接的 reduce/reduce 冲突。

%token identifier ARROW

%%

program
: expression
| program expression
;

identifier_list
: identifier
| identifier_list identifier;

lambda_arguments
: '(' identifier_list ')'
| identifier;

lambda
: lambda_arguments ARROW expression;

primary_expression
: '(' expression ')'
| identifier
| lambda;


expression : primary_expression


$ yacc -v test.6.y 
conflicts: 1 reduce/reduce

这正是因为不知道下一个符号是 ) 时要进行哪个缩减:我们是在缩减 lambda_arguments 列表还是 primary_expression?

解析器生成器通过支持 lambda 列表以错误的方式解决了它。但这意味着永远不会产生带括号的表达式。

有几种方法可以摆脱这种混乱。这可能是最简单的方法,一种不包含冲突的修改语法:

%token identifier ARROW

%%

program
: expression
| program expression
;

identifier_list
: identifier
| identifier_list identifier
;

lambda_arguments
: '(' identifier identifier_list ')'
| identifier
;

primary_expression
: '(' expression ')'
| '(' expression ')' ARROW expression
| lambda_arguments ARROW expression
| identifier
;

expression : primary_expression

我们将 lambda 语法折叠成 primary_expression,而 lambda_arguments 现在要么是一个不带括号的标识符,要么是至少两个标识符的列表。

此外,现在 lambda 有两种语法情况:

| '(' expression ')' ARROW expression
| lambda_arguments ARROW expression

因此必须编写两个语义动作规则。一些逻辑是通用的,因此可以将其外包给一个帮助函数,该函数为 lambda 构建语法树节点。

第一个语法变体的操作必须检查$2 右手符号,并检查它是否是由标识符标记组成的简单主表达式。如果是这种情况,该操作将打开表达式,取出标识符并从该标识符构建一个 lambda 列表,并使用该列表生成最终作为规则输出的 lambda 语法节点($$ 值,以 Yacc 术语)。如果 $2 是任何其他类型的表达式,则会发出诊断:这是错误的 lambda 语法,例如 ( 2 + 2 ) => foo。当然,这被解析器接受了,这就是规则被调用的方式。但它现在被 semantically 拒绝(其中 semantically 指的是“语义”一词的低热量版本)。

第二个变体的操作很简单:获取 lambda 列表、主体表达式并像以前一样创建一个 lambda 节点。

简单地说,lambda 语法与表达式语法如此紧密地集成在一起,以至于它不能轻易地被移植到完全独立的规则中,这些规则通过一个要求将lambda 简化为primary_expression 的产生式引入。这是一厢情愿的想法,因为 shift-reduce 解析器的规则不是函数调用。

【讨论】:

  • 我可以简单地禁止(x) => expr,只允许x => expr 和(x, y) => expr。
  • 你可以禁止这些。您很快就会找到包含它们的程序。
  • a) 我很高兴你去做了 LALR 生成器实验,因为它回答了这个问题。 b) 我会说你问的确切问题的答案是否定的,所以我很惊讶你第一个字写的是“是”。 c)您可以用语义动作作弊并以这种方式实现和 LALR 语法这一事实仍然对您的问题说“不”,但对“我可以构建一个以某种方式读取语言的基于 LALR 的解析器”说“是”,一个可以总是这样做,通过接受太多和(图灵)后处理来清理。所有解析器在某种程度上都以这种方式工作。
  • @IraBaxter:我的编辑清楚地表明,由于我正在考虑在新语言中引入语法相似的功能,因此我没有任何使用这种语法的现有程序。所以任何像 C# 这样的现有程序都不是我的问题。
  • 啊。然后你可以为你的方便定义语法,这个问题没有实际意义。
【解决方案4】:

我认为 lambda 表达式语法问题本身并不有趣, 除非知道该语言的 rest 是 LALR(1)。

如果您想知道答案,请将您的子语法提供给 LALR(1) 解析器 发电机。如果它抱怨 shift-reduce 或 reduce-reduce 冲突, 它不是 LALR(1)。一个语法 is LALR(1) 是否由是否决定 根据定义,您可以为其构建转换表。

大多数人都想要一个用于整个语言的解析器。

这里有两个有趣的问题。

1) C# 4.5 是否完全是一种语言 LALR(1)? (例如,是否存在 LALR(1) 的 some 语法? 请注意,特定语法不是 LALR(1) 并不意味着没有其他语法。

2) 是否有任何由 Microsoft 发布的 C# 语法(以多种形式)LALR(1)?

我认为 Eric 告诉我们 1) 不是真的。这表明 2) 也不正确。

C++ 需要无限前瞻来解析其模板,这主要是由于 ">" 被解释为“结束模板参数”或“大于”的局部可能性造成的。 由于 C# 复制了这个,我希望它对模板分辨率也有无限的前瞻要求。那绝对不是 LALR(1)。 [还有一个额外的混乱 至于“>>”是否可以被视为移位运算符,而“> >”不能,您无法在语法中修复,因为它看不到空格。]

我的公司将 GLR 用于其语言处理工具,并且我们有一个 C# 4.5 语法可以正常工作。 GLR 意味着我们不必考虑如何将上下文无关文法转换为与 LALR(1) 兼容的形式(例如,弯曲、扭曲、左/右因子、随机播放)或代码即席前瞻等。因此我们可以专注于处理代码的问题,而不是解析。

这确实意味着强制转换和其他构造在解析时会产生模棱两可的树,但如果您有类型信息,这些问题很容易解决。

【讨论】:

  • Eric 仅给出了语言规范中给出的 C# 语法中真正歧义的两个示例,这对您的第一点没有任何说明。我的直觉是,如果你不关心得到一个好的解析树,那么为 C# 制作一个 LALR(1) 语法会非常简单:例如,你可以给出一个正则表达式“id (\+ id | * id)*" 来检查一个简单的表达式语法,但这不会告诉你太多关于输入结构的信息。如果您不关心结构,则 LALR(1) 非常强大。
  • 事实上,所有的解析器都是通过某种方式读取输入,然后对结果进行后处理以克服解析器的缺点。它不能是任何其他方式(理想情况下没有后处理)。因此,您可以使用正则表达式“.+”(因此显然是 LALR)解析 C#,然后返回并添加结构,但这违背了问题的重点。我认为将其解释为“是否有一个 LALR(1) 解析器可以提取 整个 语言 [而不会在某处丢失结构]?”。对于 C#,Eric 的示例和对模板的无限前瞻几乎解决了这个问题:不。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-20
  • 2020-08-21
  • 2011-07-12
  • 2011-08-04
相关资源
最近更新 更多