Почему С++ не может быть проанализирован парсером LR (1)?
Я читал о генераторах парсеров и синтаксических анализаторах и нашел это выражение в wikipedia LR parsing -page:
Многие языки программирования могут быть проанализированы с использованием некоторой вариации парсера LR. Одним из примечательных исключений является С++.
Почему так? Какое конкретное свойство С++ приводит к невозможности синтаксического анализа парсеров LR?
Используя google, я обнаружил, что C может отлично разбираться с LR (1), но С++ требует LR (∞).
Ответы
Ответ 1
На Lambda the Ultimate есть интересная тема, в которой обсуждается грамматика LALR для C++.
Он включает в себя ссылку на докторскую диссертацию, которая включает обсуждение парсинга C++, в котором говорится, что:
"C++ грамматика неоднозначна, зависит от контекста и потенциально требует бесконечного взгляда на будущее, чтобы разрешить некоторые неясности".
Далее приводится ряд примеров (см. Стр. 147 в pdf).
Пример:
int(x), y, *const z;
имея в виду
int x;
int y;
int *const z;
Сравнить с:
int(x), y, new int;
имея в виду
(int(x)), (y), (new int));
(разделенное запятыми выражение).
Две последовательности токенов имеют одинаковую начальную подпоследовательность, но разные деревья разбора, которые зависят от последнего элемента. Перед однозначным может быть произвольно много токенов.
Ответ 2
Анализаторы LR не могут обрабатывать неоднозначные правила грамматики по дизайну. (Сделал теорию проще еще в 1970-х годах, когда идеи разрабатывались).
C и С++ допускают следующее утверждение:
x * y ;
Он имеет два разных анализа:
- Это может быть объявление y, в качестве указателя на тип x
- Он может быть умножен на х и у, отбрасывая ответ.
Теперь вы можете подумать, что последнее глупо и его следует игнорировать.
Большинство согласится с вами; однако есть случаи, когда
имеют побочный эффект (например, если умножение перегружено). но это не главное.
Дело в том, что есть два разных анализа, и поэтому программа
может означать разные вещи в зависимости от того, как это должно быть проанализировано.
Компилятор должен принять соответствующее в соответствующих обстоятельствах, а при отсутствии какой-либо другой информации (например, знание типа x) должен собираться как для того, чтобы позже решить, что делать. Таким образом, грамматика должна допускать это. И это делает грамматику неоднозначной.
Таким образом, чистый анализ LR не может справиться с этим. Также не могут использоваться многие другие широко доступные генераторы парсеров, такие как Antlr, JavaCC, YACC или традиционные Bison, или даже парсеры PEG, которые используются "чистым" способом.
Есть много более сложных случаев (синтаксический синтаксический анализ синтаксиса требует произвольного просмотра, тогда как LALR (k) может смотреть вперед не более k токенов), но только для этого требуется только один контрпример для сглаживания чистого LR (или других) синтаксического анализа.
Большинство реальных парсеров C/С++ обрабатывают этот пример, используя некоторые
вид детерминированного парсера с дополнительным взломом: они переплетают разбор с таблицей символов
коллекции... так, что к моменту появления "x",
синтаксический анализатор знает, является ли x типом или нет, и может, таким образом,
выберите между двумя возможными разборами. Но синтаксический анализатор
что делает это не контекстным, а парсеров LR
(чистые и т.д.) являются (в лучшем случае) контекстом свободными.
Можно обмануть и добавить семантические проверки времени сокращения в каждом правиле
для парсеров LR, чтобы сделать это значение. (Этот код часто не прост). Большинство других типов парсера
иметь некоторые средства для добавления семантических проверок в разных точках
в синтаксическом анализе, который можно использовать для этого.
И если вы достаточно обманываете, вы можете заставить парсеров LR работать
C и С++. Ребята из GCC на некоторое время, но дали это
для ручного кодирования, я думаю, потому что они хотели
лучшая диагностика ошибок.
Там другой подход, хотя и приятный и чистый
и анализирует C и С++ просто отлично, без таблицы символов
hackery: GLR-парсеры.
Это полные контекстные бесплатные парсеры (имеющие
смотреть вперед). Анализаторы GLR просто принимают оба анализа,
создавая "дерево" (фактически ориентированный ациклический граф, который в основном похож на дерево)
что представляет собой неоднозначный синтаксический анализ.
После анализа пар может разрешить неоднозначность.
Мы используем эту технику в интерфейсах C и С++ для наших
DMS Software Reengineering Tookit (по состоянию на июнь 2017 года
они обрабатывают полный С++ 17 в диалектах MS и GNU).
Они были использованы для обработки миллионов строк
больших систем C и С++, с полными точными разборами, которые производят АСТ с полной детализацией исходного кода. (См. АСТ для наиболее неприятного анализа Си ++.)
Ответ 3
Проблема никогда не определяется как это, тогда как это должно быть интересно:
Каков самый маленький набор модификаций в грамматике С++, который необходим, чтобы эта новая грамматика могла быть отлично проанализирована парсером yacc без контекста? (используя только один "взломать": значение имен/идентификаторов, синтаксический анализатор, сообщающий лексер каждого typedef/class/struct)
Я вижу несколько:
-
Type Type;
запрещено. Идентификатор, объявленный как имя типа, не может стать идентификатором неименования (обратите внимание, что struct Type Type
не является двусмысленным и может быть разрешено).
Существует 3 типа names tokens
:
-
types
: встроенный или из-за typedef/class/struct
- шаблон-функции
- идентификаторы: функции/методы и переменные/объекты
Рассмотрение шаблонных функций как разных токенов решает двусмысленность func<
. Если func
является именем шаблона, то <
должно быть началом списка параметров шаблона, в противном случае func
является указателем функции, а <
- оператором сравнения.
-
Type a(2);
- это экземпляр объекта.
Type a();
и Type a(int)
являются прототипами функций.
-
int (k);
полностью запрещен, следует записать int k;
-
typedef int func_type();
и
typedef int (func_type)();
запрещены.
Функция typedef должна быть указателем функции typedef: typedef int (*func_ptr_type)();
-
рекурсия шаблона ограничена 1024, в противном случае в качестве опции компилятору может быть передан увеличенный максимум.
-
int a,b,c[9],*d,(*f)(), (*g)()[9], h(char);
также может быть запрещен, заменен на int a,b,c[9],*d;
int (*f)();
int (*g)()[9];
int h(char);
одна строка для каждого прототипа функции или объявление указателя функции.
Наиболее предпочтительной альтернативой было бы изменить синтаксис ужасных функций,
int (MyClass::*MethodPtr)(char*);
ресинхронизируется как:
int (MyClass::*)(char*) MethodPtr;
это согласовано с оператором литья (int (MyClass::*)(char*))
-
typedef int type, *type_ptr;
также может быть запрещено: по одной строке на typedef. Таким образом, он станет
typedef int type;
typedef int *type_ptr;
-
sizeof int
, sizeof char
, sizeof long long
и co. могут быть объявлены в каждом исходном файле.
Таким образом, каждый исходный файл, использующий тип int
, должен начинаться с
#type int : signed_integer(4)
и unsigned_integer(4)
будут запрещены вне этой директивы #type
это было бы большим шагом в глупой нечеткости sizeof int
, присутствующей во многих заголовках С++
Компилятор, реализующий resyntaxed С++, мог бы, если столкнулся с источником С++ с использованием двусмысленного синтаксиса, переместить source.cpp
тоже в папку ambiguous_syntax
и автоматически создать однозначный перевод source.cpp
перед его компиляцией.
Пожалуйста, добавьте свои двусмысленные синтаксисы С++, если вы знаете некоторые из них.
Ответ 4
Как вы можете видеть в моем ответе здесь, С++ содержит синтаксис, который не может быть детерминистически проанализирован парсером LL или LR из-за стадии разрешения типа (обычно пост- синтаксический анализ), изменяющий порядок операций, и, следовательно, фундаментальную форму АСТ (как правило, ожидается, что она будет выполнена с помощью анализа первой ступени).
Ответ 5
Я думаю, вы очень близки к ответу.
LR (1) означает, что для синтаксического анализа слева направо требуется только один токен, чтобы смотреть вперед для контекста, тогда как LR (& infin;) означает бесконечный внешний вид. То есть, синтаксический анализатор должен был знать все, что пришло, чтобы выяснить, где оно сейчас.
Ответ 6
Проблема "typedef" в C++ может быть проанализирована с помощью синтаксического анализатора LALR (1), который создает таблицу символов во время синтаксического анализа (а не чисто синтаксический анализатор LALR). Проблема "шаблона", вероятно, не может быть решена с помощью этого метода. Преимущество этого вида синтаксического анализатора LALR (1) состоит в том, что грамматика (показанная ниже) является грамматикой LALR (1) (без двусмысленности).
/* C Typedef Solution. */
/* Terminal Declarations. */
<identifier> => lookup(); /* Symbol table lookup. */
/* Rules. */
Goal -> [Declaration]... <eof> +> goal_
Declaration -> Type... VarList ';' +> decl_
-> typedef Type... TypeVarList ';' +> typedecl_
VarList -> Var /','...
TypeVarList -> TypeVar /','...
Var -> [Ptr]... Identifier
TypeVar -> [Ptr]... TypeIdentifier
Identifier -> <identifier> +> identifier_(1)
TypeIdentifier -> <identifier> =+> typedefidentifier_(1,{typedef})
// The above line will assign {typedef} to the <identifier>,
// because {typedef} is the second argument of the action typeidentifier_().
// This handles the context-sensitive feature of the C++ language.
Ptr -> '*' +> ptr_
Type -> char +> type_(1)
-> int +> type_(1)
-> short +> type_(1)
-> unsigned +> type_(1)
-> {typedef} +> type_(1)
/* End Of Grammar. */
Следующий вход может быть проанализирован без проблем:
typedef int x;
x * y;
typedef unsigned int uint, *uintptr;
uint a, b, c;
uintptr p, q, r;
Генератор синтаксического анализатора LRSTAR считывает вышеуказанную грамматическую нотацию и генерирует синтаксический анализатор, который обрабатывает проблему "typedef" без двусмысленности в дереве синтаксического анализа или AST. (Раскрытие: я парень, который создал LRSTAR.)