Может ли эта кажущаяся неоднозначность быть проанализирована в парсере LALR (1) (PLY)? - PullRequest
0 голосов
/ 28 апреля 2018

У меня есть грамматика большого размера в PLY (Python Lexx Yacc) для языка, который имеет некоторые специфические проблемы при разборе. Язык позволяет ведущему синтаксису двух видов вызовов выглядеть одинаково почти до конца нетерминального вызова. Это дает много возможностей для уменьшения / уменьшения конфликтов, поскольку семантика токенов на этом пути различна, но может быть построена из одних и тех же терминальных токенов. Я извлек простые версии до / после грамматики ниже, что я объясню немного.

Первоначально выражения представляли собой типичную "стратифицированную грамматику", которая брала вызовы и литералы и тому подобное в первичное, затем первичное через унарное, затем двоичное в общее выражение. Проблема заключалась в том, что Call_expr с двумя аргументами конфликтует с версией Iter_expr, которая начинается с двух идентификаторов перед '/'. Конфликт был в запятой после первого аргумента в вызове, так как изначально Expr -> ... -> Primary_expr -> Name_expr -> Id было разрешено. Синтаксический анализатор может либо уменьшить Id до Expr, чтобы соответствовать Call_expr, либо оставить его равным Iter_expr. Забегая вперед, запятая не помогла решить. Если первый аргумент вызова - это просто идентификатор (например, переменная), это допустимая двусмысленность. Рассмотрим ввод id > id ( id , id ....

Мой подход состоял в том, чтобы создать такое выражение, которое не могло бы быть просто Id. Я добавил производственную цепочку во все выражения, чтобы дать им «_nn» версии - «не имя». Затем я мог бы определить продукцию для Call_expr, которая использует любой синтаксис в первом аргументе, который делает его больше, чем просто имя (например, операторы, вызовы и т. Д.), Чтобы уменьшить его до BinOp_expr_nn, а также разрешить производство вызовов, которое просто имеет Id в качестве первого аргумента. Это должно убедить парсер просто сдвинуться, пока он не сможет разрешить либо Iter_expr, либо Call_expr (или, по крайней мере, знать, по какому пути он идет.)

Как вы уже догадались, это все портит :). Работая с цепочкой выражений, также работает с Primary_expr, который мне все еще нужно уменьшить до Id. Но теперь это конфликт уменьшения / уменьшения - каждый Primary_expr может остаться там или перейти к Unary_expr. Я могу приказать им сделать выбор (что может сработать), но я ожидаю, что в итоге я буду гоняться за другим и другим.

Итак, мой вопрос: есть ли метод, который кто-то может сформулировать о том, как разрешить, чтобы одни и те же токены представляли разные семантики (например, expr vs id), которые все еще можно анализировать с помощью LALR (1), например, PLY? Если не считать каких-либо полезных хаков, которые помогут справиться с проблемой? Может ли это быть неоднозначным?

terminals:  '+' '^' ',' '>' '(' ')' '/' ':' 'id' 'literal' 
   (i.e. punctuation (besides '->' and '|', initial-lower-case words)
non-terminals:  initial-Upper-case words

Оригинальная грамматика:

S'-> S
S -> Call_expr
   | Iter_expr
Expr -> BinOp_expr
BinOp_expr -> Unary_expr
BinOp_expr -> BinOp_expr '+' BinOp_expr
Unary_expr -> Primary_expr
   | '^' BinOp_expr
Primary_expr -> Name_expr
   | Call_expr
   | Iter_expr
   | Literal_expr
Name_expr -> Id
Args -> Expr
   | Args ',' Expr
Call_expr -> Primary_expr '>' Id '(' ')'
   | Primary_expr '>' Id '(' Args ')'
Iter_expr -> Primary_expr '>' Id '(' Id '/' Expr ')'
   | Primary_expr '>' Id '(' Id ':' Id '/' Expr ')'
   | Primary_expr '>' Id '(' Id ',' Id ':' Id '/' Expr ')'
Literal_expr -> literal
Id -> id

Моя попытка устранить неоднозначность первого аргумента в Call_expr:

S'-> S
S -> Call_expr
   | Iter_expr
Expr -> BinOp_expr_nn
   | BinOp_expr
BinOp_expr -> BinOp_expr_nn
   | Unary_expr
BinOp_expr_nn -> Unary_expr_nn
   | BinOp_expr '+' BinOp_expr
Unary_expr -> Primary_expr
   | Unary_expr_nn
Unary_expr_nn -> Primary_expr_nn
   | '^' BinOp_expr
Primary_expr -> Primary_expr_nn
   | Name_expr
Primary_expr_nn -> Call_expr
   | Iter_expr
   | Literal_expr
Name_expr -> Id
Args -> Expr
   | Args ',' Expr
Call_expr -> Primary_expr '>' Id '(' ')'
   | Primary_expr '>' Id '(' Expr ')'
   | Primary_expr '>' Id '(' Id , Args ')'
   | Primary_expr '>' Id '(' BinOp_expr_nn , Args ')'
Iter_expr -> Primary_expr '>' Id '(' Id '/' Expr ')'
   | Primary_expr '>' Id '(' Id ':' Id '/' Expr ')'
   | Primary_expr '>' Id '(' Id ',' Id ':' Id '/' Expr ')'
Literal_expr -> literal
Id -> id

1 Ответ

0 голосов
/ 29 апреля 2018

Несмотря на заголовок вашего поста, ваша грамматика не является двусмысленной. Это просто не LR (1) по той причине, которую вы упомянули: вход

A ( B , 

может быть началом Call_expr или Iter_expr. В первом случае значение B должно быть уменьшено до Expr, а затем до Args; во втором случае его нельзя уменьшать, поскольку он должен быть равен id, когда правая часть id '(' id ',' id ':' id '/' Expr ')' уменьшена. Решение нельзя принять, просто взглянув на один жетон предпросмотра (, ), поэтому возникает конфликт сдвиг-уменьшение.

Этот конфликт может быть разрешен не более чем двумя дополнительными жетонами предпросмотра, поскольку смещение является допустимым действием, только если за , следует точно id, а затем : . Так что это делает грамматику LALR (3). К сожалению, Ply не генерирует парсеры LALR (3) (как и yacc / bison), но есть некоторые альтернативы.

1. Используйте другой алгоритм разбора

Поскольку грамматика однозначна, она может быть проанализирована без проблем (и без каких-либо изменений) с помощью синтаксического анализатора GLR. Ply также не производит парсеры GLR, но Bison может. Это вряд ли будет вам полезно, но я подумал, что стоит упомянуть об этом, если вы не используете Python.

2. Используйте грамматику, которая допускает некоторые неверные входные данные, и отбросьте их с помощью семантического анализа

Это почти наверняка самое простое решение, и это то, что я обычно рекомендую. Если вы измените определение Iter_expr на:

Iter_expr : id '(' id '/' Expr ')'
          | id '(' Args ':' id '/' Expr ')'

, тогда он все равно будет распознавать каждый действительный ввод (поскольку id и id , id являются действительными экземплярами Args). Это устраняет конфликт сдвиг-уменьшение; по сути, он позволяет анализатору избегать принятия решения, пока он не встретит ) (что будет означать Call_expr) или : (что означает Iter_expr) , (С первой альтернативой для Iter_expr проблем нет, поскольку решение о смещении / вместо уменьшения id не требует дополнительного просмотра.)

Конечно, вторая продукция для Iter_expr распознает множество вещей, которые не являются допустимыми итерационными выражениями: списки из более чем 2 элементов и списки, которые включают выражения, более сложные, чем просто один id. Однако эти входные данные вообще не являются действительными программами, поэтому их можно просто отклонить в действии для Iter_expr. Точный код для распознавания действительной итерации будет зависеть от того, как вы представляете свой AST, но это не сложно: просто убедитесь, что длина Args равна одному или двум, и что каждый элемент в списке является просто id.

3. Используйте лексический хак

Один из способов восполнить недостаток информации о предварительном просмотре - это собрать ее в лексере, собрав необходимые данные в буфере и выводя лексемы только тогда, когда известна их синтаксическая категория. В этом случае лексер может найти последовательность '(' id ',' id ':' и отметить первую id, чтобы ее можно было использовать только в Iter_expr. В этом случае единственное изменение в грамматике:

Iter_expr : id '(' id '/' Expr ')'
          | id '(' id ':' id '/' Expr ')'
          | id '(' iter_id ',' id ':' id '/' Expr ')'

Хотя в данном конкретном случае это будет хорошо работать, оно не очень легко обслуживаемо. В частности, он основан на возможности определить простой и однозначный шаблон, который может быть реализован в лексере. Поскольку этот шаблон является упрощением грамматики, вполне возможно, что какое-то будущее синтаксическое добавление создаст синтаксис, который также будет соответствовать тому же шаблону. (Это называется лексическим «взломом» по причине.)

4. Найдите LALR (1) грамматику

Как указано, эта грамматика LALR(3). Но не существует такой вещи, как LALR(3) язык . Точнее, если язык имеет грамматику LALR(k), то он также имеет грамматику LALR(1), и эта грамматика может быть создана механически из грамматики LALR(k). Более того, при анализе разборочной грамматики из одного символа можно восстановить дерево разбора для исходной грамматики.

Поскольку грамматики механического производства довольно большие, эта процедура не очень привлекательна, и я не знаю о реализации алгоритма. Вместо этого чаще всего пытаются переписать только часть грамматики, как вы пытались в первоначальном вопросе. Конечно, это можно сделать, но конечный результат не совсем интуитивен.

Однако это не так сложно. Вот, например, немного упрощенная версия вашей грамматики с удаленными избыточными производственными единицами и парой исправлений в приоритете операторов (возможно, некорректных, поскольку я не знаю, на какую семантику вы нацеливаетесь).

Продукция, названия которой заканчиваются на N, не производит ID. Для каждого из них есть соответствующая продукция без N, которая добавляет ID. После этого необходимо было переписать Args для использования продукции ExprN, а также разрешить различные списки аргументов, начинающиеся с одного или двух ID s. Производство Chain было сделано только для того, чтобы избежать повторения.

Start   : Call
        | Iter
Expr    : ID
        | ExprN
ExprN   : UnaryN
        | Expr '+' Unary
Unary   : ID
        | UnaryN
UnaryN  : ChainN
        | '^' Chain
Chain   : ID
        | ChainN
ChainN  : PrimaryN
        | Chain '>' CallIter
PrimaryN: LITERAL
        | Call
        | Iter
        | '(' Expr ')'
Call    : ID '(' ')'
        | ID '(' ID ')'
        | ID '(' ID ',' ID ')'
        | ID '(' Args ')'
Iter    : ID '(' ID '/' Expr ')'
        | ID '(' ID ':' ID '/' Expr ')'
        | ID '(' ID ',' ID ':' ID '/' Expr ')'
Args    : ExprN ExprList
        | ID ',' ExprN ExprList
        | ID ',' ID ',' Expr ExprList
ExprList:
        | ExprList ',' Expr

Я не проверял это так сильно, как хотел бы, но я думаю, что он дает правильный язык.

...