Левосторонняя грамматика кофейных выражений - PullRequest
4 голосов
/ 25 ноября 2011

Я пишу парсер Antlr / Xtext для грамматики coffeescript . Это еще в начале, я только что переместил подмножество оригинальной грамматики , и я застрял с выражениями. Это страшная ошибка "выражение правила имеет не-LL (*) решение". Я нашел несколько связанных с этим вопросов здесь: Справка по левому разложению грамматики для удаления левой рекурсии и Грамматика ANTLR для выражений . Я также попробовал Как убрать глобальный возврат из вашей грамматики , но он просто демонстрирует очень простой случай, который я не могу использовать в реальной жизни. Пост о ANTLR Grammar Tip: LL () и 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
  ;

UPDATE: Окончательное решение будет использовать Xtext с внешним лексером, чтобы избежать сложностей обработки значительных пробелов . Вот фрагмент из моей версии Xtext:

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

Моя стратегия - сначала создать работающий анализатор Antlr без пригодного для использования AST. (Ну, это заслуживает отдельного вопроса, если это осуществимый подход.) Так что на данный момент мне нет дела до токенов, они включены для облегчения разработки.

Я знаю, что оригинальная грамматика - LR. Я не знаю, насколько близко я могу остаться к нему при преобразовании в LL.

ОБНОВЛЕНИЕ2 и РЕШЕНИЕ: Я мог бы упростить мою проблему с пониманием, полученным из ответа Барта. Вот рабочая грамматика игрушки для обработки простых выражений с вызовами функций, чтобы проиллюстрировать это. Комментарий до 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 ')'
  ;

1 Ответ

4 голосов
/ 25 ноября 2011

Когда вы генерируете лексер и парсер из вашей грамматики, вы видите следующую ошибку, напечатанную на вашей консоли:

ошибка (211): CoffeeScript.g: 52: 3: [фатальный] выражение правила имеет решение без LL (*) из-за рекурсивных вызовов правил, достижимых из alts 1 , 3. Решить с помощью левого факторинга или используя синтаксические предикаты или используя параметр backtrack = true.

предупреждение (200): CoffeeScript.g: 52: 3: Решение может соответствовать вводу, такому как "{NUMBER, STRING}", используя несколько альтернатив: 1, 3

В результате альтернативный вариант (ы) 3 были отключены для этого входа

(я подчеркнул важные биты)

Это только первая ошибка, но вы начинаете с первой и, если повезет, ошибки ниже первой также исчезнут, когда вы исправите первую.

Вышеприведенная ошибка означает, что когда вы пытаетесь проанализировать NUMBER или STRING с анализатором, сгенерированным из вашей грамматики, анализатор может пойти двумя путями, когда он окажется в правиле expression :

expression
  : value      // choice 1
  | assign     // choice 2
  | operation  // choice 3
  ;

А именно, вариант 1 и вариант 3 могут анализировать NUMBER или STRING, как вы можете видеть по "путям", по которым анализатор может следовать, чтобы соответствовать этим двум вариантам:

выбор 1:

expression
  value
    literal
      alphaNumeric : {NUMBER, STRING}

выбор 3:

expression
  operation
    logicOp
      relationOp
        shiftOp
          additiveOp
            mathOp
              questionOp
                term
                  value
                    literal
                      alphaNumeric : {NUMBER, STRING}

В последней части предупреждения ANTLR информирует вас о том, что он игнорирует вариант 3 всякий раз, когда NUMBER или STRING будет проанализирован, в результате чего выбор 1 будет соответствовать такому вводу (так как он определен до выбора 3) .

Итак, либо грамматика CoffeeScript неоднозначна в этом отношении (и каким-то образом обрабатывает эту неоднозначность), либо ваша реализация неверна (я предполагаю последнее :)). Вам необходимо исправить эту неоднозначность в вашей грамматике: не допускайте, чтобы варианты expression 1 и 3 совпадали с одним и тем же вводом.


Я заметил еще 3 вещи в вашей грамматике:

1

Примите следующие правила лексера:

NEW : 'new';
...
UNARY : '!' | '~' | NEW;

Имейте в виду, что токен UNARY никогда не может соответствовать тексту 'new', поскольку токен NEW определен перед ним. Если вы хотите, чтобы UNARY сделал это, удалите правило NEW и выполните:

UNARY : '!' | '~' | 'new';

2

В некоторых случаях вы собираете несколько типов токенов в один, например LOGIC:

LOGIC : '&&' | '||';

и затем вы используете этот токен в правилах синтаксического анализа:

logicOp
  : compareOp (LOGIC compareOp)*
  ;

Но если вы собираетесь оценить такое выражение на более позднем этапе, вы не знаете, что соответствует этому LOGIC токену ('&&' или '||'), и вам придется проверить внутреннее состояние токена текст, чтобы узнать это. Вам лучше сделать что-то вроде этого (по крайней мере, если вы делаете какую-то оценку на более позднем этапе):

AND : '&&';
OR  : '||';

...

logicOp
  : compareOp ( AND compareOp // easier to evaluate, you know it's an AND expression
              | OR  compareOp // easier to evaluate, you know it's an OR expression
              )*
  ;

3

Вы пропускаете пробелы (и без табуляции?) С:

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

но разве CoffeeScript не отступает, это блок кода с пробелами (и табуляциями), как Python? Но, возможно, вы собираетесь сделать это на более позднем этапе?


Я только что увидел, что грамматика, которую вы смотрите - это грамматика jison (которая более или менее реализована в JavaScript). Но бизон и, следовательно, jison генерируют парсеры LR , а ANTLR генерирует парсеры LL . Поэтому попытка придерживаться правил исходной грамматики приведет только к большему количеству проблем.

...