Обработка неоднозначных, но все же нарушающих синтаксис при разборе выражений - PullRequest
0 голосов
/ 28 апреля 2018

Контекст

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

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

Мой синтаксический анализатор использует рекурсивный спуск для разбора выражений.

Проблема

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

comparison → addition ( ( ">" | ">=" | "<" | "<=" ) addition )* ;

- это правило, которое я использую для анализа узлов сравнения в выражении

С другой стороны, я решил разобрать родовые выражения следующим образом:

generic → primary ( "<" arguments ">" ) ;

, где

arguments → expression ( "," expression )* ;

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

Например, в a<2 он будет анализировать «a» в качестве основного элемента типа идентификатора, сразу же после этого найдет синтаксис для универсального типа, проанализирует его и потерпит неудачу, так как не может найти закрывающий тег.

Каково решение такого сценария? Особенно в таких языках, как C ++, где у дженериков также могут быть выражения, если я не ошибаюсь arr<1<2> может быть допустимым синтаксисом.

Это особый крайний случай или требуется изменение определения синтаксиса, о котором я не знаю?

Спасибо

1 Ответ

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

например, в <2 он будет анализировать «a» как первичный элемент типа идентификатора, а затем немедленно найти синтаксис для универсального типа, проанализировать его и потерпеть неудачу, так как не может найти закрывающий тег </p>

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

Однако этот подход больше не работает, если обе интерпретации синтаксически верны (то есть синтаксис на самом деле неоднозначен). Одним из примеров этого может быть x<y>z, который может быть либо объявлением переменной z типа x<y>, либо двумя сравнениями. Этот пример несколько проблематичен, так как последнее значение почти никогда не подразумевается, поэтому хорошо всегда интерпретировать его как первое (это происходит, например, в C #).

Теперь, если мы допустим выражения, это станет более сложным. Для x<y>z достаточно просто сказать, что это никогда не должно интерпретироваться как сравнение двух, так как нет смысла сравнивать результат сравнения с чем-то другим (во многих языках использование реляционных операторов на логических значениях в любом случае является ошибкой типа). Но для чего-то вроде a<b<c>() есть две интерпретации, которые могут быть обе действительны: либо a является универсальной функцией, вызываемой с универсальным аргументом b<c, либо b является универсальной функцией с универсальным аргументом ca сравнивается с результатом вызова этой функции). На данный момент уже невозможно разрешить эту неоднозначность только с помощью синтаксических правил:

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

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

Поскольку ни один из них не идеален, некоторые языки изменяют синтаксис для вызова обобщенных функций с явными параметрами типа, чтобы сделать его LL (1). Например:

  • Java помещает общий список аргументов метода перед именем метода, то есть obj.<T>foo() вместо obj.foo<T>().
  • Rust требует :: перед общим списком аргументов: foo::<T>() вместо foo<T>().
  • Scala использует квадратные скобки для обобщений и ни для чего другого (для индексов массива используются скобки): foo[T]() вместо foo<T>().
...