我想问的问题是标题中简明扼要。让我举一个有问题的语法的例子:
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 ')'
)。
编辑:实际上,我正在考虑窃取使用这个语法小节来获得新语言中的类似功能。我已经在语法形式上有类似JavaScript的匿名函数但是我的豚鼠吱吱声反馈抱怨它们对于许多用途来说太冗长了,并且指出C#lambda表达式是更理想的解决方案。我担心这个解决方案可能导致模糊不清。所以,真的,我只对那个小节感兴趣。其他东西,如泛型和演员表对我来说都不是问题。
我语法的早期版本是可机械分析的,我不想失去这个属性,而我之前使用机械发生器的经验告诉我,最好先在这里查看,而不是试试自己。对于我的手动解析器,我当然可以只使用特殊情况'(' identifier
来比正常情况更进一步。
答案 0 :(得分:37)
首先,解析器理论始终是我的弱点之一。我主要从事语义分析工作。
其次,我曾经使用过的所有C#解析器都是手工生成的递归下降解析器。我之前在解析器理论方面有着强大背景的同事之一确实构建了自己的解析器生成器,并成功地将C#语法输入其中,但我不知道这样做会有什么样的恶劣攻击。
所以我在这里说的是以适当的怀疑态度来回答这个问题。
正如您所注意到的,lambdas稍微有点烦恼,因为您必须小心这个带括号的表达式 - 它可能是带括号的表达式,强制转换运算符或lambda参数列表,而lambda参数列表可能是几个不同的形式。但考虑到所有事情,在C#3.0中添加lambda是相对容易的,语法上的;破解解析器并不是太难 - 这是语义分析,它是lambdas的熊。
就前瞻性而言,C#语法中真正令人烦恼的问题是泛型和强制转换。
在语言已经有>>
,>
和<
运算符后,C#2中添加了泛型,当您将泛型投入混合时,所有这些都会导致奇怪的问题。 / p>
经典问题当然是A ( B < C, D > ( E ) )
方法A
的调用是否有两个参数:B < C
和D > (E)
或一个,B<C,D>( E )
?
消除歧义的规则是:
如果可以将一系列标记解析为以type-argument-list结尾的简单名称,成员访问或指针成员访问,则会检查紧跟在结束
>
标记之后的标记。如果它是( ) ] : ; , . ? == !=
之一,那么type-argument-list将作为simple-name,member-access或pointer-member-access的一部分保留,并且将丢弃令牌序列的任何其他可能的解析。否则,type-argument-list不被视为simple-name,member-access或pointer-member-access的一部分,即使没有其他可能的标记序列解析。
语法的第二个问题可以追溯到C#1.0,那就是演员操作符。问题是(x)-y
可能意味着“将-y
强制转换为x
”,或者它可能意味着从y
中减去x
。这里的规则是:
括号中的一个或多个标记的序列只有在满足以下条件之一时才被视为强制转换表达式的开头:
令牌序列是一种类型的正确语法,但不适用于表达式。
标记序列是类型的正确语法,紧跟在右括号后面的标记是标记“〜”,标记“!”,标记“(”,标识符,文字或任何关键字除了
as
和is
。
消除两种情况歧义的规则在理论上涉及潜在的大前瞻,但在实践中,您很少需要备份解析器。
答案 1 :(得分:9)
用C#-style lambdas增强的表达式语法不是LALR(1),但它可能是LALR(2)。因此,生成等效的LALR(1)语法是可能的(尽管不一定是微不足道的):见下面的编辑。
您将在输入上获得减少/减少冲突:
( id )
因为id
可以缩减为identifier_list
或expression
(间接地,在第二种情况下),并且解析器无法根据一个先行标记来判断哪一个是正确的( )
)。
它可以基于两个前瞻代币来判断,因为identifier_list
减少只有在第二个下一个代币为=>
时才有效,并且只要=>
不是您的运算符语言,如果第二个下一个标记为expression
,则无法=>
减少。所以我认为这可能是LALR(2),虽然我不能肯定地说。
有多个标识符的情况没有问题,因为在
中( id1 id2 )
id1 id2
不能简化为表达式(在大多数表达语言中;当然,你的可能会有所不同)。如果“=&gt;”,单个未标注的标识符紧跟=>
后面的情况也没有问题不是有效的运营商。
修改强>
我在原来的答案中忽略了提到没有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
中第一个expression_list
的冲突减少/减少,而有限前瞻无法解决,因为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
;
我没有完整的语法,因为我不知道目标语言需要什么。
答案 2 :(得分:5)
是的,这种情况是直接减少/减少冲突。
%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
现在是单个未加密码的标识符,或者至少包含两个标识符的列表。
此外,lambdas现在有两个语法案例:
| '(' expression ')' ARROW expression
| lambda_arguments ARROW expression
因此必须编写两个语义动作规则。一些逻辑是常见的,因此可以将其归结为辅助函数,该函数为lambda构建语法树节点。
第一个句法变体的动作必须检查$2
右手符号,并检查它是否是由标识符标记组成的简单主表达式。如果是这种情况,则操作会破解表达式,取出标识符并从该标识符构建lambda列表,并使用该列表生成最终作为规则输出的lambda语法节点({{1}以Yacc术语表示的值)。如果$$
是任何其他类型的表达式,则会发出诊断:它是错误的lambda语法,例如$2
。当然,解析器接受了这一点,这就是调用规则的方式。但它现在被语义拒绝(其中语义指的是“语义”一词的低卡路里版本。)
第二个变体的动作很简单:使用lambda列表,正文表达式并创建一个lambda节点,就像之前一样。
简单地说,lambda语法紧密集成到表达式语法中,不能轻易地将其整合到完全独立的规则中,这些规则通过单个生成引入,该生成要求( 2 + 2 ) => foo
减少为{{1 }}。这是一厢情愿的想法,因为shift-reduce解析器的规则不是函数调用。
答案 3 :(得分:3)
我不认为lambda表达式语法问题本身很有趣, 除非知道该语言的 rest 是LALR(1)。
如果您想知道答案,请将您的子语法提供给LALR(1)解析器 发电机。如果它抱怨shift-reduce或reduce-reduce冲突, 它不是LALR(1)。语法是否 LALR(1)取决于是否 根据定义,您可以为它构建转换表。
大多数人都想要整个语言的解析器。
这里有两个有趣的问题。
1)C#4.5作为语言LALR(1)吗? (例如,是否某些语法是LALR(1)? 请注意,特定语法不是LALR(1)并不意味着没有另一个语法。
2)Microsoft发布的任何C#语法(多种形式)LALR(1)?
我认为埃里克告诉我们1)不是真的。这表明2)也不是真的。
C ++需要无限的前瞻来解决其模板,主要是由于“&gt;”的本地可能性造成的被解释为“结束模板args”或“大于”。 由于C#复制了这个,我希望它对模板解析也有无限的前瞻性要求。这绝对不是LALR(1)。 [还有一个混乱 至于“&gt;&gt;”是否可以视为移位运算符,“&gt;&gt;”不能,你不能修复语法,因为它看不到空格。]
我的公司使用GLR作为其语言处理工具,我们有一个C#4.5语法可以正常工作。 GLR意味着我们不必考虑如何将无上下文语法转换为LALR(1)兼容形式(例如,弯曲,扭曲,左/右因子,随机播放),或者代码特设前瞻等。因此我们可以专注于处理代码的问题,而不是解析。
它确实意味着强制转换和其他构造在分析时产生不明确的树,但如果您有类型信息,这些很容易解决。