【问题标题】:Left-factoring grammar of coffeescript expressionscoffeescript 表达式的左分解语法
【发布时间】:2024-01-21 19:23:01
【问题描述】:

我正在写Antlr/Xtext parser for coffeescript grammar。还处于开始阶段,我只是移动了original grammar 的一个子集,我被表达式困住了。这是可怕的“规则表达式具有非 LL(*) 决策”错误。我在这里找到了一些相关的问题,Help with left factoring a grammar to remove left recursionANTLR Grammar for expressions。我也试过How to remove global backtracking from your grammar,但它只是演示了一个我在现实生活中无法使用的非常简单的案例。关于ANTLR Grammar Tip: LL() and Left Factoring 的帖子让我有了更多的见解,但我仍然无法掌握。

我的问题是如何修复以下语法(对不起,我无法简化它并仍然保留错误)。我猜麻烦制造者是term 规则,所以我很欣赏它的本地修复,而不是改变整个事情(我试图保持接近原始语法的规则)。也欢迎指点如何在你的头脑中“调试”这种错误的语法。

grammar CoffeeScript;

options {
  output=AST;
}

tokens {
  AT_SIGIL; BOOL; BOUND_FUNC_ARROW; BY; CALL_END; CALL_START; CATCH; CLASS; COLON; COLON_SLASH; COMMA; COMPARE; COMPOUND_ASSIGN; DOT; DOT_DOT; DOUBLE_COLON; ELLIPSIS; ELSE; EQUAL; EXTENDS; FINALLY; FOR; FORIN; FOROF; FUNC_ARROW; FUNC_EXIST; HERECOMMENT; IDENTIFIER; IF; INDENT; INDEX_END; INDEX_PROTO; INDEX_SOAK; INDEX_START; JS; LBRACKET; LCURLY; LEADING_WHEN; LOGIC; LOOP; LPAREN; MATH; MINUS; MINUS; MINUS_MINUS; NEW; NUMBER; OUTDENT; OWN; PARAM_END; PARAM_START; PLUS; PLUS_PLUS; POST_IF; QUESTION; QUESTION_DOT; RBRACKET; RCURLY; REGEX; RELATION; RETURN; RPAREN; SHIFT; STATEMENT; STRING; SUPER; SWITCH; TERMINATOR; THEN; THIS; THROW; TRY; UNARY; UNTIL; WHEN; WHILE;
}

COMPARE : '<' | '==' | '>';
COMPOUND_ASSIGN : '+=' | '-=';
EQUAL : '=';
LOGIC : '&&' | '||';
LPAREN  :   '(';
MATH : '*' | '/';
MINUS : '-';
MINUS_MINUS : '--';
NEW : 'new';
NUMBER  :   ('0'..'9')+;
PLUS : '+';
PLUS_PLUS : '++';
QUESTION : '?';
RELATION : 'in' | 'of' | 'instanceof'; 
RPAREN  :   ')';
SHIFT : '<<' | '>>';
STRING  :   '"' (('a'..'z') | ' ')* '"';
TERMINATOR : '\n';
UNARY : '!' | '~' | NEW;
// Put it at the end, so keywords will be matched earlier
IDENTIFIER :    ('a'..'z' | 'A'..'Z')+;

WS  :   (' ')+ {skip();} ;

root
  : body
  ;

body
  : line
  ;

line
  : expression
  ;

assign
  : assignable EQUAL expression
  ;

expression
  : value
  | assign
  | operation
  ;

identifier
  : IDENTIFIER
  ;

simpleAssignable
  : identifier
  ;

assignable
  : simpleAssignable
  ;

value
  : assignable
  | literal
  | parenthetical
  ;

literal
  : alphaNumeric
  ;

alphaNumeric
  : NUMBER 
  | STRING;

parenthetical
  : LPAREN body RPAREN
  ;

// term should be the same as expression except operation to avoid left-recursion
term
  : value
  | assign
  ;

questionOp
  : term QUESTION?
  ;

mathOp
  : questionOp (MATH questionOp)*
  ;

additiveOp
  : mathOp ((PLUS | MINUS) mathOp)*
  ;

shiftOp
  : additiveOp (SHIFT additiveOp)*
  ;

relationOp
  : shiftOp (RELATION shiftOp)*
  ;

compareOp
  : relationOp (COMPARE relationOp)*
  ;

logicOp
  : compareOp (LOGIC compareOp)*
  ;

operation
  : UNARY expression
  | MINUS expression
  | PLUS expression
  | MINUS_MINUS simpleAssignable
  | PLUS_PLUS simpleAssignable
  | simpleAssignable PLUS_PLUS
  | simpleAssignable MINUS_MINUS
  | simpleAssignable COMPOUND_ASSIGN expression
  | logicOp
  ;

更新: 最终的解决方案将使用带有外部词法分析器的 Xtext 来避免intricacies of handling significant whitespace。这是我的 Xtext 版本中的一个 sn-p:

CompareOp returns Operation:
  AdditiveOp ({CompareOp.left=current} operator=COMPARE right=AdditiveOp)*;

我的策略是首先制作一个没有可用 AST 的 Antlr 解析器。 (好吧,如果这是一种可行的方法,那么值得提出一个单独的问题。)所以我现在不关心令牌,它们被包含在内是为了让开发更容易。

我知道原始语法是 LR。我不知道在变身为LL时我能离它有多近。

UPDATE2 和解决方案: 我可以通过从 Bart 的回答中获得的见解来简化我的问题。这是一个有效的玩具语法,用于处理带有函数调用的简单表达式来说明它。 expression 之前的评论显示了我的洞察力。

grammar FunExp;

ID: ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*;
NUMBER: '0'..'9'+;
WS: (' ')+ {skip();};

root
  : expression
  ;

// atom and functionCall would go here,
// but they are reachable via operation -> term
// so they are omitted here
expression
  : operation
  ;

atom
  : NUMBER
  | ID
  ;

functionCall
  : ID '(' expression (',' expression)* ')'
  ;

operation
  : multiOp
  ;

multiOp
  : additiveOp (('*' | '/') additiveOp)*
  ;

additiveOp
  : term (('+' | '-') term)*
  ;

term
  : atom
  | functionCall
  | '(' expression ')'
  ;

【问题讨论】:

标签: antlr coffeescript grammar xtext left-recursion


【解决方案1】:

当您从语法生成词法分析器和解析器时,您会看到控制台打印出以下错误:

error(211): CoffeeScript.g:52:3: [fatal] rule expression 由于递归规则调用可从替代 1,3。通过左分解或使用语法谓词或使用 backtrack=true 选项来解决。

warning(200): CoffeeScript.g:52:3: Decision 可以使用多种选择来匹配“{NUMBER, STRING}”等输入:1、3

因此,alternative(s) 3 被禁用该输入

(我已经强调了重要的部分)

这只是第一个错误,但你从第一个开始,如果运气好的话,当你修复第一个错误时,第一个下面的错误也会消失。

上面发布的错误意味着当您尝试使用从您的语法生成的解析器来解析 NUMBERSTRING 时,当解析器以 expression 规则结束时,它可以采用两种方式:

表达式
  : 值 // 选择 1
  |分配 // 选择 2
  |操作 // 选择 3
  ;

也就是说,选项 1 和选项 3 都可以解析 NUMBERSTRING,正如您可以通过解析器可以遵循的“路径”来匹配这两个选项:

选择 1:

表达式
  价值
    文字
      字母数字:{NUMBER, STRING}

选择 3:

表达式
  手术
    逻辑运算
      关系操作
        移位操作
          加法运算
            数学运算
              问题操作
                学期
                  价值
                    文字
                      字母数字:{NUMBER, STRING}

在警告的最后一部分,ANTLR 通知您,无论何时解析 NUMBERSTRING,它都会忽略选项 3,从而导致选项 1 匹配此类输入(因为它是在选项 3 之前定义的) .

所以,要么 CoffeeScript 语法在这方面是模棱两可的(并且以某种方式处理这种模棱两可),要么你的实现是错误的(我猜是后者:))。您需要修正语法中的这种歧义:即不要让 expression 的选择 1 和 3 都匹配相同的输入。


我注意到你的语法中还有 3 件事:

1

采用以下词法分析器规则:

新:'新';
...
一元:'!' | '~' |新的;

请注意,标记 UNARY 永远不会匹配文本 'new',因为标记 NEW 是在它之前定义的。如果您想让UNARY 解决这个问题,请删除NEW 规则并执行以下操作:

一元:'!' | '~' | '新';

2

在某些情况下,您会在一个单一的集合中收集多种类型的令牌,例如 LOGIC

逻辑:'&&' | '||';

然后您在解析器规则中使用该标记,如下所示:

逻辑运算
  :比较操作(逻辑比较操作)*
  ;

但是如果你要在稍后阶段评估这样的表达式,你不知道这个 LOGIC 标记匹配什么('&amp;&amp;''||'),你必须检查标记的内部文本以找出答案。你最好做这样的事情(至少,如果你在稍后阶段进行某种评估):

AND : '&&';
或:'||';

...

逻辑运算
  : compareOp ( AND compareOp // 更容易计算,你知道它是一个 AND 表达式
              | OR compareOp // 更容易计算,你知道它是一个 OR 表达式
              )*
  ;

3

您正在跳过空格(没有制表符?):

WS : (' ')+ {skip();} ;

但是 CoffeeScript 不是像 Python 那样用空格(和制表符)缩进它的代码块吗?但也许你会在稍后阶段这样做?


我刚刚看到the grammar you're looking at 是一个jison 语法(它或多或少是JavaScript 中的一个bison 实现)。但是野牛,因此 jison,生成 LR parsers,而 ANTLR 生成 LL parsers。所以试图贴近原始语法的规则只会导致更多的问题。

【讨论】:

  • 您对错误消息和解析器路径的解释帮助我理解了这个问题,并使看似神秘的 Antlr 消息更有意义。在语法上向您的 cmets 表示,我对词法分析器部分还不感兴趣(请参阅我的更新)。我仍然觉得你的第一条评论很有用,我不知道。
  • @Adam,我猜你的语法有歧义,但我错了:CoffeeScript 充满了歧义!看起来单个标识符是有效的语句(作为隐式返回值),导致解析器可能将单个标识符解析为返回语句、赋值的开始或函数调用(仅举出 3 种可能性)。这是一种难于解析的语言,但在使用它一段时间后,它确实让 JavaScript 变得更加有趣!一种可爱的小语言!真不错。
  • @AdamSchmideg,最后提醒一句:我还看到像f 1 + f 2 这样的输入是有效的,应该被解释为f(1 + f(2))。从右到左匹配输入的 LR 解析器在这方面几乎没有问题,但尝试使用 LL 解析器这样做会变得笨拙。但是在原始的 CoffeeScript 语法中还有很多东西不起作用:例如,LR 语法中的左递归在 LL 语法中根本不起作用。我确信这不是不可能的,但是用 ANTLR 语法编写一个像样的 CoffeeScript 语法会变得很困难。当然,祝你好运!
  • 感谢您的 cmets 和建议。我想我会在此过程中发布更多问题:)
最近更新 更多