Можно ли проанализировать эту кажущуюся двусмысленность в анализаторе LALR(1) (PLY)?

У меня есть большая грамматика в 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 против 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

person jspencer    schedule 28.04.2018    source источник
comment
Я бы просто объединил Iter_Expr и Call_Expr в один синтаксис, а затем разобрался, что это такое, пройдясь по AST. Если вы знаете Лисп, это должно быть очевидно. Другими словами, разрешите эти формы Id : Id / Expr в синтаксисе Call_Expr. Затем пройдитесь по узлам Call_Expr, чтобы определить, действительно ли они являются действительными Iter_exprs. Это приведет к чрезмерной генерации: разрешить недопустимое использование Id : Id / Expr; Вы можете проверить это и поставить диагноз.   -  person Kaz    schedule 29.04.2018
comment
@rici: извините за ошибки, но вы интерпретировали наиболее правильно. '›' должен быть в списке терминалов; он отделяет одни звонки от других, которых нет в моей игрушечной версии. Все эти вызовы могут быть объединены в цепочку ala foo › bar(baz / a + baz) › qux(b, c, d, e), и каждая левая часть '›' представляет собой primary_expr. '/' — это еще один синтаксический разделитель, а не оператор. Извините за путаницу: большая часть этого исходит из упрощений и вне контекста, я уверен, что это кажется действительно странным.   -  person jspencer    schedule 29.04.2018
comment
Я все еще думаю, что вы хотите BinOp_expr -> Unary_expr | BinOp_expr '+' Unary_expr (чтобы + было менее ассоциативным; как написано, это неоднозначно) и Unary_expr -> Primary_expr | '^' Unary_expr, чтобы ^ a + b означало (^a) + b, а не ^(a + b)). (Или, возможно, Unary_expr -> Primary_expr | '^' Primary_expr, если ^^a не разрешено.)   -  person rici    schedule 29.04.2018
comment
Вы также можете захотеть Primary_expr -> '(' Expr ')', чтобы разрешить скобки.   -  person rici    schedule 29.04.2018
comment
Да, я рассматриваю их. На данный момент я использую приоритет, чтобы решить все это, но поиграю с устранением неоднозначности. Спасибо вам и @Kaz за ваши замечательные предложения. Очень признателен, и я должен быть на моем пути сейчас!   -  person jspencer    schedule 30.04.2018
comment
@jspencer: объявления приоритета решат проблему с бинарными операторами, но не с унарными операторами (если вы выделили Unary_expr, как в вашем примере).   -  person rici    schedule 30.04.2018
comment
@jspencer: Кроме того, я добавил грамматику LALR (1) без конфликтов, что, как мне кажется, дает правильный язык.   -  person rici    schedule 30.04.2018


Ответы (1)


Несмотря на название вашего сообщения, ваша грамматика не двусмысленна. Это просто не 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. Производство 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

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

person rici    schedule 29.04.2018
comment
Спасибо за такую ​​тщательность. Мне нравится идея более явной грамматики, но она действительно приводит ко многим конфликтам редукции/редукции, требующим тщательно упорядоченных правил. Я даже не уверен, что смогу заставить все это работать (представьте, что a + b + c, разобранный на Expr '+' UnaryN . '+' 'c'. Если мы применим ExprN -> UnaryN, мы сдвинемся и выполним b +c, чтобы стать ExprN. Теперь нет способа сократить Expr '+' Unary). Учитывая сложность грамматики и необходимость так тщательно упорядочивать и настраивать, считаете ли вы, что лучше пойти по этому пути или просто открыть грамматику и разобраться с вещами, изучив AST/семантику? - person jspencer; 01.05.2018
comment
@jspencer: я расположил решения в приблизительном порядке привлекательности и прямо сказал во втором варианте, что я бы порекомендовал именно его (если вы не можете использовать генератор GLR). - person rici; 01.05.2018
comment
@jspencer: Однако я проверил предоставленную мной грамматику. Он анализирует x(a+b+c) как ('Call', ['x', '(', ('Args', [('ExprN', [('ExprN', ['a', '+', 'b']), '+', 'c'])]), ')']), что правильно, если предположить, что ваш оператор + является левоассоциативным. (Примечание: конструктор синтаксического дерева, который я использовал, не показывает сокращения единиц.) - person rici; 01.05.2018
comment
Возможно, лучшим примером является a + b › c() + d (которое должно быть + (b › c()) + d. У меня, вероятно, есть другие сложности из-за полной грамматики, но я сталкиваюсь со многими такими случаями. где любой выбор правильный в разных случаях. Я до сих пор не понимаю, как предотвратить конфликты и вместо этого заставить синтаксический анализатор идти (всегда) по правильному пути. По крайней мере, не в полномасштабной грамматике ( ~160 правил) Больше борьбы! :) - person jspencer; 01.05.2018
comment
@jspencer: Этот тоже отлично работает. Я проанализировал x(a+b>c()+d) (поскольку ваша грамматика требует call или iter в качестве формы верхнего уровня), и результирующий анализ был фактически x((a+(b>c()))+d). Что заставляет вас думать, что это было бы неправильно? Моя программа Ply тривиальна; это в основном грамматика, которую я уже вставил. Но если вы действительно не можете это воспроизвести, я мог бы выложить это где-нибудь. - person rici; 02.05.2018
comment
Вы очень любезны. Я уверен, что ваш тест правильный. В моей полной грамматике есть дополнительные сложности (которые я не опубликовал и не понял до конца!), которые, несомненно, являются проблемой. Симптом, который я продолжаю получать, похоже, происходит из-за возможности использовать Chain -> ChainN и `UnaryN -> ChainN' или другие подобные варианты. В одних случаях хочется одного, в других другого. Например, мои литералы могут включать Expr. Я все еще изо всех сил пытаюсь интуитивно понять, как ограничивать вещи, поэтому есть только один выбор. Но я думаю, что я должен просто бороться с этим, пока это не станет ясно. Общие советы приветствуются! - person jspencer; 02.05.2018
comment
Давайте продолжим обсуждение в чате. - person jspencer; 02.05.2018