it-swarm.com.ru

Почему C++ не может быть проанализирован с помощью анализатора LR (1)?

Я читал о парсерах и генераторах парсеров и нашел это утверждение на странице анализа LR в Википедии:

Многие языки программирования могут быть проанализированы с использованием некоторого варианта синтаксического анализатора LR. Одним заметным исключением является C++.

Почему это так? Какое специфическое свойство C++ делает невозможным анализ парсеров LR?

Используя Google, я только обнаружил, что C может быть отлично проанализирован с помощью LR (1), но C++ требует LR (∞). 

141
Cheery

На 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));

(разделенное запятыми выражение).

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

87
Rob Walker

Парсеры LR не могут обрабатывать неоднозначные правила грамматики по своему замыслу. (Облегчил теорию еще в 1970-х годах, когда разрабатывались идеи).

C и C++ допускают следующее утверждение:

x * y ;

У этого есть два различных анализа:

  1. Это может быть объявление у, как указатель на тип х
  2. Это может быть умножение на x и y, отбрасывая ответ.

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

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

Таким образом, чистый анализ LR не может справиться с этим. Многие другие широко доступные генераторы синтаксических анализаторов, такие как Antlr, JavaCC, YACC, традиционные Bison или даже синтаксические анализаторы в стиле PEG, не могут использоваться «чисто».

Есть много более сложных случаев (синтаксический анализ синтаксиса шаблона требует произвольного просмотра, в то время как LALR (k) может смотреть вперед на большинство k токенов), но только один контрпример требует сброса pure LR (или других) разбор.

Большинство реальных синтаксических анализаторов C/C++ обрабатывают этот пример, используя некий детерминированный синтаксический анализатор .__ с дополнительным хаком: они переплетаются с таблицей символов Collection ..., так что к тому времени, когда встречается «x», синтаксический анализатор знает, является ли x типом или нет, и поэтому может выбирать между двумя потенциальными синтаксическими анализами. Но парсер , Который делает это, не является контекстно-свободным, а парсеры LR (Чистые и т.д.) (В лучшем случае) контекстно-свободны.

Можно выполнить читерство и добавить семантические проверки времени сокращения для каждого правила в К анализаторам LR, чтобы сделать это устранение неоднозначности. (Этот код часто не прост). Большинство других типов синтаксического анализатора Имеют некоторые средства для добавления семантических проверок в различных точках при разборе, которые можно использовать для этого.

И если вы обманываете достаточно, вы можете заставить парсеры LR работать на C и C++. Ребята из GCC сделали это на некоторое время, но дали это Для анализа, закодированного вручную, я думаю, потому что они хотели Лучшую диагностику ошибок.

Однако есть и другой подход: хороший и чистый Он прекрасно разбирает C и C++ без таблицы символов Hackery: анализаторы GLR . Это синтаксические анализаторы без контекста (имеющие эффективно бесконечно с нетерпением). Синтаксические анализаторы GLR просто принимают both синтаксические разборы, Производят «дерево» (на самом деле ориентированный ациклический граф, в основном древовидный) , Которое представляет неоднозначный анализ. Пропуск после разбора может устранить неоднозначности.

Мы используем эту технику в интерфейсах C и C++ для нашего Tookit по реинжинирингу программного обеспечения DMS (по состоянию на июнь 2017 года Они обрабатывают полный C++ 17 в MS и GNU диалекты) ..__ Они использовались для обработки миллионов строк Больших систем C и C++ с полными, точными разборками, производящими AST с полными деталями исходного кода. (См. AST для самого неприятного анализа C++. )

223
Ira Baxter

Проблема никогда не определяется так, хотя это должно быть интересно:

каков наименьший набор модификаций в грамматике C++, который был бы необходим для того, чтобы эта новая грамматика могла быть идеально проанализирована "неконтекстно-свободным" синтаксическим анализатором yacc? (использование только одного «хака»: неоднозначность typename/identifier, синтаксический анализатор, информирующий лексер о каждом typedef/class/struct)

Я вижу несколько из них:

  1. Type Type; запрещен. Идентификатор, объявленный как имя типа, не может стать идентификатором без имени (обратите внимание, что struct Type Type не является неоднозначным и все еще может быть разрешен).

    Существует 3 типа names tokens:

    • types: встроенный тип или из-за typedef/class/struct
    • шаблон-функции
    • идентификаторы: функции/методы и переменные/объекты

    Рассмотрение шаблонных функций как различных токенов решает неоднозначность func<. Если func является именем функции шаблона, тогда < должен быть началом списка параметров шаблона, в противном случае func является указателем на функцию, а < является оператором сравнения.

  2. Type a(2); - это экземпляр объекта. Type a(); и Type a(int) являются прототипами функций.

  3. int (k); полностью запрещена, должна быть написана int k;

  4. typedef int func_type(); и typedef int (func_type)(); запрещены. 

    Функция typedef должна быть указателем функции typedef: typedef int (*func_ptr_type)();

  5. рекурсия шаблона ограничена 1024, в противном случае увеличенный максимум может быть передан компилятору в качестве опции.

  6. 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*))

  7. typedef int type, *type_ptr; также может быть запрещен: одна строка для typedef. Таким образом, это станет

    typedef int type; 

    typedef int *type_ptr; 

  8. sizeof int, sizeof char, sizeof long long и co. может быть объявлено в каждом исходном файле . Таким образом, каждый исходный файл, использующий тип int, должен начинаться с

    #type int : signed_integer(4)

    и unsigned_integer(4) будет запрещен вне этой директивы #type Это будет большой шаг в глупой двусмысленности sizeof int, присутствующей во многих заголовках C++

Компилятор, реализующий повторно синтаксис C++, при обнаружении источника C++, использующего неоднозначный синтаксис, переместит source.cpp тоже в папку ambiguous_syntax и автоматически создаст однозначно переведенный source.cpp перед компиляцией.

Пожалуйста, добавьте свои неоднозначные синтаксисы C++, если вы их знаете!

12
reuns

Как вы можете видеть в моем ответе здесь , C++ содержит синтаксис, который не может быть детерминированно проанализирован синтаксическим анализатором LL или LR из-за стадии разрешения типа (обычно после анализа), изменяющей порядок операций , и следовательно, основная форма AST (обычно ожидается, что она будет получена при разборе на первом этапе).

8
Sam Harwell

Я думаю, что вы довольно близки к ответу. 

LR (1) означает, что для разбора слева направо требуется только один токен для просмотра контекста, в то время как LR (∞) означает бесконечный просмотр вперед. То есть парсер должен был знать все, что приходило, чтобы выяснить, где он сейчас.

5
casademora

Проблема «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 .)

0
Paul B Mann