是C#的lambdaexpression式语法LALR(1)?
我希望问的问题在标题中得到了肯定。 让我举一个有关语法的例子:
identifier_list : identifier | identifier_list identifier; lambda_arguments : '(' identifier_list ')' | identifier; lambda : lambda_arguments '=>' expression
然后我们添加正常的C语言expression语法 – 特别是,
primary_expression : '(' expression ')' | identifier | lambda;
真正的问题是,这个语法LALR(1)是可parsing的,即能够被自动parsing器生成器parsing吗? 还是需要手动滚动或GLRparsing器? 请注意,我希望具体了解这一小节,而不是上下文相关的关键字或其他部分。
我现在想的是,如果parsing器看到'(' identifier ')'
,这有两个有效的parsing,所以如果parsing器看到identifier
,向前看')'
,它将无法决定parsing树下去。 这可能只是一个转换/减less冲突,但是,我可以通过分配一些任意优先级(可能有利于'(' identifier ')'
)来消除。
编辑:其实,我正在考虑用新语言中的这个语法分段来窃取类似的function。 我已经有类似于JavaScript的匿名函数的语法forms,但是我的天竺鼠吱吱声反馈抱怨说他们对于很多用途来说太冗长了,并且指出C#lambdaexpression式是更理想的解决scheme。 我担心这个解决scheme可能导致模糊。 所以,真的,我只对这一部分感兴趣。 其他类似generics和演员对我来说不是问题。
以前版本的语法是机械可分解的,我不想失去这个属性,而我之前使用机械发生器的经验告诉我,最好在这里检查一下,而不是尝试自己。 对于我的手滚parsing器,我当然可以简单地使用特殊情况'(' identifier
比正常情况稍微向前看一些。
用C#风格的lambdaexpression式增加的expression式语法不是LALR(1),但可能是LALR(2)。 因此,生成一个等价的LALR(1)语法是可能的(尽pipe不一定是微不足道的):参见下面的编辑。
你将得到一个input减less/减less的冲突:
( id )
因为id
可以简化为identifier_list
或expression
(间接的,在第二种情况下),并且parsing器根据一个先行标记( )
不能分辨哪一个是正确的。
它可以基于两个前瞻符号来说明,因为只有在第二个下一个符号=>
identifier_list
减less才有可能,只要=>
不是语言中的运算符,那么如果第二个下一个符号是=>
。 所以我认为这可能是LALR(2),虽然我不能肯定地说。
因为有多个标识符的情况是没有问题的
( id1 id2 )
id1 id2
不能简化为一个expression式(在大多数expression式语言中,当然可能不同)。 如果一个单一的未被标识的标识符后面紧接着=>
,那么假设'=>'不是一个有效的运算符,也是没有问题的。
编辑
在我原来的回答中,我忽略了提到没有LALR(2)语言这样的东西。 LALR(2)语法识别的语言也被一些LALR(1)语法识别。 事实上,这个断言有一个build设性的certificate,它允许机械创build这样一个LALR(1)语法,以及恢复原始parsing树的过程。
在这种情况下,生成LALR(1)文法就更简单了,因为如上所述,只有一个生产需要额外的预测。 解决办法是延迟一个令牌的减less。 换句话说,在原始语法中包含如下内容:
primary: '(' expression ')' lambda_parameters: '(' id_list ')'
id_list
和expression
派生terminalID
。 除ID
之外,这两个非terminal的派生是不相交的,所以我们可以这样解决这个问题:
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中的语法生成具有多个参数的lambdas,作为不以逗号分隔的标识符序列: (ab) => a + b
。 我认为实际意图是使用逗号: (a, b) => a + b
,这就是我在上面的语法中所做的。 如果你的语言有一个逗号运算符,那么区别就很重要,因为在这种情况下,expression式可能是'(' expression_list ')'
,它与lambda参数列表冲突。 一个简单的实现会导致expression
中的第一个expression
的减less/减less冲突,由于expression_list
可能是任意长的,所以不能用有限的前瞻来解决。
不过,这种情况也有一个解决scheme:它将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 ;
但是我没有写出完整的语法,因为我不知道目标语言要求什么。
首先,parsing器理论总是我的一个弱点。 我主要工作在语义分析器上。
其次,我所做过的所有C#parsing器都是手工生成的recursion下降parsing器。 我以前在parsing器理论方面拥有很强的背景的同事中,有一位build立了自己的parsing器生成器,并成功地将C#语法join其中,但是我不知道这样做会带来什么样的恶意攻击。
所以我在这里说的是以适当的怀疑态度来回答这个问题。
就像你注意到的那样,lambdaexpression式有点令人费解,因为你必须小心这个带括号的expression式 – 它可能是一个加括号的expression式,一个转换操作符或一个lambda参数列表,lambda参数列表可能有几种不同的forms。 但是考虑到所有事情,在C#3.0中添加lambdaexpression式在语法上相对容易; 破解parsing器并不是太困难 – 语义分析对于lambda来说是一个负担。
就C#语法而言,真正令人头疼的问题就是generics和cast 。
在C#2中添加generics,在语言已经具有>>
, >
和<
运算符之后,所有这些在将generics引入混合时都会引起奇怪的问题。
经典问题当然是A ( B < C, D > ( E ) )
方法A
的调用有两个参数: B < C
和D > (E)
还是一个, B<C,D>( E )
?
消除歧义的规则是:
如果可以将一系列标记parsing为以types参数列表结束的简单名称,成员访问或指针成员访问,则会检查紧接在closures
>
标记之后的标记。 如果是( ) ] : ; , . ? == !=
( ) ] : ; , . ? == !=
( ) ] : ; , . ? == !=
那么types参数列表被保留为简单名称,成员访问或指针成员访问的一部分,任何其他可能的令牌序列parsing都将被丢弃。 否则,即使没有其他可能的令牌序列parsing,types参数列表也不被视为简单名称,成员访问或指针成员访问的一部分。
语法的第二个问题可以追溯到C#1.0,那就是演员。 问题是(x)-y
可能意味着“铸造 – 键入x
”或它可能意味着从x
减去y
。 这里的规则是:
只有在满足以下至less一个条件的情况下,括在括号内的一个或多个令牌的序列才被认为是cast-expression的开始:
令牌的顺序是一个types的正确语法,但不是一个expression式。
令牌的顺序是一个types的正确的语法,紧接着括号后面的标记是标记“〜”,标记“!”,标记“(”,标识符,文字或除了
is
。
消除这两种情况的规则在理论上涉及到很大的前瞻性,但在实践中,你很less必须将parsing器备份到很远的地方。
是的,这种情况是一个简单的减less/减less冲突。
%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
这正是不知道下一个符号是什么时候减less的)
:我们是在减less一个lambda_arguments
列表还是一个primary_expression
?
parsing器生成器已经通过偏好lambda列表以错误的方式解决了它。 但是这意味着一个加上括号的expression是永远不会产生的。
这个混乱有几种方法。 这可能是最简单的方法,一个不包含冲突的修改过的语法:
%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
现在可以是一个单独的未标识的标识符,或者是至less两个标识符的列表。
此外,lambdaexpression式现在有两个语法情况:
| '(' expression ')' ARROW expression | lambda_arguments ARROW expression
所以必须写出两个语义动作规则。 一些逻辑将是常见的,所以它可以被扩展到为lambda构build语法树节点的辅助函数。
第一个语法变体的动作必须检查$2
右手符号,并检查它是一个由标识符标记组成的简单主expression式。 如果是这种情况,操作会打开expression式,取出标识符并从该标识符中构build一个lambda列表,并使用该列表生成以规则的输出结尾的lambda句法节点( $$
值,在Yacc条款)。 如果$2
是任何其他types的expression式,则会发出诊断:这是错误的lambda语法,如( 2 + 2 ) => foo
。 当然,这被parsing器接受了,这是如何调用规则的。 但它现在正在语义上被拒绝( 语义上指的是“语义”一词的低卡路里版本)。
第二个变体的操作很简单:像以前一样,使用lambda表,bodyexpression式和lambda节点。
简而言之,lambda语法如此紧密地集成到expression式语法中,使得它不能被简单地分解成完全独立的规则,这些规则是通过一个生产引入的,这个生产要求将lambda
简化为primary_expression
。 这是一厢情愿的想法,因为shift-reduce分析器的规则不是函数调用。
我不认为lambdaexpression式语法问题本身是有趣的,除非知道其余的语言是LALR(1)。
如果您想知道答案,请将您的语法提交给LALR(1)parsing器生成器。 如果它抱怨减less冲突或减less冲突,那不是LALR(1)。 语法是否为 LALR(1)取决于是否可以为其构build转换表。
大多数人想要整个语言的parsing器。
这里有两个有趣的问题。
1)C#4.5是一种语言LALR(1)吗? (例如,是否有一些语法是LALR(1)?注意,不是LALR(1)的特定语法并不意味着没有另一个语法。
2)Microsoft(以其多种forms)LALR(1)发布的任何C#语法?
我认为埃里克告诉我们1)是不正确的。 这表明2)也是不正确的。
C ++需要无限的前瞻来parsing它的模板,很大程度上是由“>”的本地可能性被解释为“结束模板参数”或“大于”引起的。 由于C#复制这个,我希望它也有模板parsing无限的超前需求。 这绝对不是LALR(1)。 [关于“>>”是否可以当作移位运算符,还有一个额外的问题,“>>”不能,因为看不到空格,所以不能修复语法。]
我的公司使用GLR作为其语言处理工具,而且我们有一个C#4.5语法运行良好。 GLR意味着我们不必考虑如何将上下文无关的语法转换为与LALR(1)兼容的forms(例如,弯曲,扭转,左/右因子,随机播放),或者代码特定的前瞻等等。因此我们可以专注于处理代码的问题,而不是parsing。
这确实意味着演员和其他构造在parsing时会产生不明确的树,但是如果您有types信息,则这些树很容易解决。