Чому не можна розібрати C ++ за допомогою аналізатора LR (1)?


153

Я читав про аналізатори та генератори парсерів і знайшов це твердження на LR-розборі сторінки Вікіпедії:

Багато мов програмування можна проаналізувати, використовуючи деяку варіацію аналізатора LR. Один помітний виняток - C ++.

Чому так? Яка особливість C ++ спричиняє неможливість розбору з LR-аналізаторами?

Використовуючи google, я лише виявив, що C може бути ідеально розібраний з LR (1), але для C ++ потрібен LR (∞).


7
Так само, як: вам потрібно зрозуміти рекурсію, щоб навчитися рекурсії ;-).
Toon Krijthe

5
"Ви зрозумієте парсери, як тільки розберете цю фразу."
ілля н.

Відповіді:


92

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

(вираз, відокремлений комою).

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


29
Було б здорово мати на цій сторінці короткий підсумок про сторінку 147. Я збираюся прочитати цю сторінку. (+1)
Веселий

11
Приклад: int (x), y, * const z; // значення: int x; int y; int * const z; (послідовність оголошень) int (x), y, new int; // значення: (int (x)), (y), (new int)); (вираз, відокремлений комою) Дві послідовності лексеми мають однакове початкове підряд, але різні дерева розбору, які залежать від останнього елемента. Перед розрізненням може бути безліч лексем.
Blaisorblade

6
Ну, в цьому контексті ∞ означає "довільно багато", тому що lookahead завжди буде обмежений довжиною введення.
MauganRa

1
Я дуже спантеличений цитатою, витягнутою з кандидатської дисертації. Якщо є неоднозначність, то, за визначенням, НІ lookahead ніколи не може "вирішити" неоднозначність (тобто вирішити, який синтаксичний розбір є правильним, оскільки принаймні 2 синтаксиси граматикою вважаються правильними). Більше того, цитування згадує про неоднозначність C, але пояснення не показує неоднозначності, а лише неоднозначний приклад, коли рішення про розбір може бути прийняте лише після довільного тривалого погляду вперед.
додекаплекс

231

LR-парсери не можуть впоратися з неоднозначними граматичними правилами за конструкцією. (Полегшило теорію ще в 1970-х, коли ідеї розроблялися).

І C, і C ++ дозволяють наступне твердження:

x * y ;

Він має два різних синтаксиси:

  1. Це може бути оголошення y, як вказівник на тип x
  2. Це може бути множення на x і y, викинувши відповідь.

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

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

Таким чином, чистий LR-аналіз не може впоратися з цим. Не можна також використовувати багато інших широко доступних генераторів парсера, таких як Antlr, JavaCC, YACC або традиційні аналізатори стилю Bison, або навіть PEG-стилі в чистому вигляді.

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

Більшість реальних парсерів C / C ++ обробляють цей приклад, використовуючи якийсь детермінований аналізатор із додатковим хаком: вони переплітаються з розбором з колекцією таблиць символів ... так що до моменту виникнення "x", аналізатор знає, чи x є типом чи ні, і таким чином можна вибирати між двома потенційними розборами. Але аналізатор, який робить це, не є контекстним, а LR-аналізатори (чисті тощо) є (у кращому випадку) контекстом вільними.

Можна зробити обман і додати семантичні перевірки на скорочення часу за правило в парсерів LR, щоб зробити це розбірливість. (Цей код часто не простий). Більшість інших типів синтаксичного аналізу мають певні засоби для додавання семантичних перевірок у різні точки розбору, які можна використовувати для цього.

І якщо ви досить обдурите, ви можете змусити LR-парсери працювати на C і C ++. Хлопці з GCC робили деякий час, але здавали його на ручний кодований аналіз, я думаю, тому що вони хотіли кращої діагностики помилок.

Хоча є ще один підхід, який є приємним і чистим і аналізує C і C ++ просто чудово, без будь-яких хакерських таблиць символів: GLR-парсери . Це повні контекстні парсери (мають ефективно нескінченний пошук). GLR-аналізатори просто приймають обидва синтаксиси, створюючи "дерево" (насправді спрямований ациклічний графік, який переважно схожий на дерево), який представляє неоднозначний аналіз. Пропуск після розбору може вирішити двозначності.

Ми використовуємо цю техніку на передніх кінцях C і C ++ для нашого програмного забезпечення для реінжинірингу програмного забезпечення DMS (станом на червень 2017 року ці повні C ++ 17 в діалектах MS та GNU). Вони були використані для обробки мільйонів рядків великих систем C і C ++, з повними, точними аналізами, що створюють AST з повними деталями вихідного коду. (Див . AST для найбільш розгульного розбору C ++. )


11
Хоча приклад 'x * y' цікавий, те ж саме може трапитися і в C ('y' може бути typedef або змінною). Але C може бути проаналізований парсером LR (1), тож яка різниця з C ++?
Мартін Кот

12
Мій анссер вже зауважив, що у С була така ж проблема, я думаю, ви це пропустили. Ні, він не може бути розбитий LR (1) з тієї ж причини. Е, що ти означає, що "у" може бути typedef? Можливо, ви мали на увазі "х"? Це нічого не змінює.
Іра Бакстер

6
Синтаксичний аналіз 2 не обов'язково дурний в C ++, оскільки * може бути відмінено, щоб мати побічні ефекти.
Dour High Arch

8
Я подивився x * yі посміхнувся - дивно, як мало хто думає про подібні маленькі двозначності.
new123456

51
@altie Напевно, ніхто не перевантажує оператора зсуву бітів, щоб змусити його записувати найбільш змінні типи в потік, правда?
Troy Daniels

16

Проблема ніколи не визначається як така, тоді як вона повинна бути цікавою:

який найменший набір модифікацій граматики С ++, необхідних для того, щоб ця нова граматика могла бути ідеально проаналізована "безконтекстним" аналізатором yacc? (використовуючи лише один 'хак': розбірливість імені типу / ідентифікатора, аналізатор, що інформує лексеру кожного типу 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І Ко. може бути оголошено у кожному вихідному файлі. Таким чином, intслід починати кожен вихідний файл, що використовує тип

    #type int : signed_integer(4)

    і unsigned_integer(4)було б заборонено поза цією #type директивою, це було б великим кроком до дурної sizeof intнеоднозначності, присутньої у багатьох заголовках C ++

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

Будь ласка, додайте свої неоднозначні синтаксиси C ++, якщо ви їх знаєте!


3
C ++ занадто добре закріпився. Ніхто цього не зробить на практиці. Ті люди (як ми), які будують передні кінці, просто кусають кулю і роблять інженерію для того, щоб парсери працювали. І поки існують шаблони в мові, ви не збираєтеся отримувати чистий контекстний аналізатор.
Іра Бакстер

9

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


3
Технологія розбору, яка займається двозначністю, просто створює обидва варіанти AST під час їх розбору, а також просто усуває неправильний варіант залежно від інформації про тип.
Іра Бакстер

@Ira: Так, це правильно. Особлива перевага цього полягає в тому, що ви можете зберегти розділення першого розбору. Хоча це найвідоміше в парному аналізаторі GLR, я не маю жодної конкретної причини, за якою я бачу, що ви не могли вдарити C ++ за допомогою "GLL?" розбираємо також.
Сем Харвелл

"GLL"? Ну, звичайно, але вам доведеться розібратися з теорією і написати документ, що залишився для використання. Швидше за все, ви можете використовувати кодований аналізатор зверху вниз або аналізатор зворотного відстеження LALR () (але зберігати «відхилений») синтаксис або запустити аналізатор Ерлі. GLR має перевагу в тому, що це досить прокляте хороше рішення, добре задокументовано і на сьогоднішній день добре доведено. Технологія GLL повинна мати деякі істотні переваги для відображення GLR.
Іра Бакстер

Проект Rascal (Нідерланди) стверджує, що вони будують аналізатор GLL без сканера. Незавершена робота може бути складно знайти будь-яку інформацію в Інтернеті. en.wikipedia.org/wiki/RascalMPL
Іра Бакстер

@IraBaxter Там , здається, нові розробки по GLL: подивитися 2010 документ про GLL dotat.at/tmp/gll.pdf
Sjoerd

6

Я думаю, ви досить близькі до відповіді.

LR (1) означає, що для розбору зліва направо для розгляду контексту потрібен лише один маркер, тоді як LR (means) означає нескінченний погляд вперед. Тобто аналізатору слід було б знати все, що відбувається, щоб зрозуміти, де він зараз.


4
З мого класу компіляторів я пригадую, що LR (n) для n> 0 математично зводиться до LR (1). Це не вірно для n = нескінченності?
rmeador

14
Ні, є непрохідна гора різниці між російською і нескінченною.
ефемієнт

4
Чи не відповідь: Так, дається нескінченна кількість часу? :)
Стів Фаллоуз

7
Насправді, з мого неясного згадування про те, як відбувається LR (n) -> LR (1), це передбачає створення нових проміжних станів, тому час виконання - це якась незмінна функція 'n'. Переклад LR (inf) -> LR (1) зайняв би нескінченний час.
Аарон

5
"Чи не відповідь: Так, дається нескінченна кількість часу?" - Ні: словосполучення "дається нескінченна кількість часу" - це просто нечуттєвий, короткий спосіб сказати "не можна робити за певну кількість часу". Коли ви бачите "нескінченне", подумайте: "не будь-яке кінцеве".
ChrisW

4

Проблема "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 аналізатора зчитує вище граматику позначення і генерує синтаксичний аналізатор , який обробляє «ЬурейеЕ» проблема без неоднозначності синтаксичного дерева або АСТ. (Розкриття: Я - хлопець, який створив LRSTAR.)


Це стандартний хак, який використовує GCC зі своїм колишнім аналізатором LR для вирішення неоднозначності речей, таких як "x * y;" На жаль, все ще існує довільно велика вимога пошуку для розбору інших конструкцій, тому LR (k) не може бути рішенням жодного фіксованого k. (GCC перейшов на рекурсивний спуск із більшою кількістю рекламних переслідувань).
Іра Бакстер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.