Я провів багато досліджень за останні кілька днів, щоб краще зрозуміти, чому існують ці окремі технології та які їх сильні та слабкі сторони.
Деякі з уже існуючих відповідей натякали на деякі їх відмінності, але вони не давали повної картини і, здавалося, були дещо сумнівними, саме тому ця відповідь була написана.
Ця експозиція довга, але важлива. потерпіть зі мною (Або якщо ви нетерплячі, прокрутіть до кінця, щоб побачити блок-схему).
Щоб зрозуміти відмінності між Parser Combinators і Parser Generators, спочатку потрібно зрозуміти різницю між різними видами розбору, які існують.
Розбір
Парсинг - це процес аналізу рядка символів відповідно до формальної граматики. (In Computing Science) розбір використовується для того, щоб комп'ютер міг зрозуміти текст, написаний мовою, зазвичай створюючи дерево розбору, яке представляє написаний текст, зберігаючи значення різних письмових частин у кожному вузлі дерева. Це дерево розбору може бути використане для різних цілей, таких як переклад його на іншу мову (використовується у багатьох компіляторах), інтерпретація письмових інструкцій безпосередньо певним чином (SQL, HTML), дозволяючи таким інструментам, як Linters,
робити свою роботу та ін. Іноді дерево розбору не є явнимзгенеровано, але, скоріше, дія, яка повинна бути виконана на кожному типі вузла в дереві, виконується безпосередньо. Це підвищує ефективність, але під водою все ще існує неявний синтаксичний розбір.
Розбір - проблема, обчислювальна обчислювальна. На цю тему було проведено понад п’ятдесят років досліджень, але ще багато чому навчитися.
Грубо кажучи, існують чотири загальні алгоритми, які дозволяють комп'ютеру проаналізувати вхід:
- LL розбір (Без контексту, розбір зверху вниз.)
- LR розбір (Без контексту, розбір знизу вгору.)
- PEG + Packrat розбору.
- Ерлі Парсінг.
Зауважимо, що ці типи розбору є дуже загальними, теоретичними описами. Існує кілька способів реалізації кожного з цих алгоритмів на фізичних машинах з різними компромісами.
LL та LR можуть переглядати лише граматики без контексту (тобто контекст навколо записаних лексем не важливо, щоб зрозуміти, як вони використовуються).
Синтаксичний розбір PEG / Packrat та синтаксис Ерлі використовуються набагато менше: Ерлі-синтаксичний аналіз симпатичний тим, що він може обробляти набагато більше граматик (включаючи ті, які необов'язково безконтекстні), але він менш ефективний (як стверджує дракон книга (розділ 4.1.1); я не впевнений, чи ці твердження все ще є точними).
Граматика синтаксичного розбору + синтаксичний аналіз - це метод, який є відносно ефективним і може також обробляти більше граматик, ніж LL та LR, але приховує амбіції, як це швидко торкнеться нижче.
LL (зліва направо, ліве виведення)
Це, мабуть, самий природний спосіб подумати про розбір. Ідея полягає в тому, щоб переглянути наступний маркер у вхідному рядку, а потім вирішити, який з можливих декількох можливих рекурсивних викликів слід прийняти для створення структури дерева.
Це дерево побудоване "зверху вниз", це означає, що ми починаємо з кореня дерева, і пересуваємо правила граматики так само, як ми рухаємося через вхідний рядок. Це також можна розглядати як побудову еквівалента 'postfix' для потоку лексем 'infix', який читається.
Парсери, що виконують синтаксичний розбір у стилі LL, можуть бути записані так, щоб вони були схожі на вказану оригінальну граматику. Це дозволяє порівняно легко їх зрозуміти, налагодити та покращити. Комбінатори класичних парсерів - це не що інше, як «лего шматки», які можна скласти для створення аналізатора стилю LL.
LR (зліва направо, право праворуч)
LR-синтаксичний аналіз рухається іншим способом знизу вгору: На кожному кроці верхній елемент (-и) стека порівнюються зі списком граматики, щоб побачити, чи можна їх звести
до правила вищого рівня в граматиці. Якщо ні, наступний маркер з вхідного потоку зміщується ед і розміщується у верхній частині стека.
Програма є правильною, якщо наприкінці ми закінчуємо одним вузлом на стеці, який представляє початкове правило з нашої граматики.
Дивитися вперед
В будь-якій з цих двох систем іноді доводиться зазирнути більше токенів із вхідних даних, перш ніж мати можливість вирішити, який вибір зробити. Це (0)
та (1)
, (k)
або (*)
-синтаксис, який ви бачите після назв цих двох загальних алгоритмів, таких як LR(1)
або LL(k)
. k
зазвичай означає «стільки, скільки потрібно вашій граматиці», тоді як *
зазвичай означає «цей аналізатор виконує зворотний трек», який є більш потужним / простим у здійсненні, але має значно більшу пам’ять та використання часу, ніж аналізатор, який може просто тримати розбір лінійно.
Зауважте, що у парсерів стилю LR вже є багато жетонів на стеці, коли вони можуть вирішити "заздалегідь", тому вони вже мають більше інформації для відправки. Це означає, що вони часто потребують менш "lookahead", ніж аналізатор стилю LL для тієї ж граматики.
LL vs. LR: Неоднозначність
Читаючи два описаних вище опису, можна задатися питанням, чому існує розбір у стилі LR, оскільки розбір у стилі LL здається набагато природнішим.
Однак при розборі стилю LL є проблема: ліва рекурсія .
Дуже природно писати граматику на кшталт:
expr ::= expr '+' expr | term
term ::= integer | float
Але, аналізатор стилю LL застрягне в нескінченному рекурсивному циклі під час розбору цієї граматики: При спробі самої лівої можливості expr
правила він знову повторюється до цього правила, не витрачаючи жодного вводу.
Є способи вирішити цю проблему. Найпростіше - переписати свою граматику, щоб подібні рекурсії більше не відбувалися:
expr ::= term expr_rest
expr_rest ::= '+' expr | ϵ
term ::= integer | float
(Тут ϵ означає «порожній рядок»)
Ця граматика зараз є рекурсивною. Зауважте, що це негайно читати набагато складніше.
На практиці ліва рекурсія може відбуватися опосередковано при багатьох інших кроках між ними. Це робить важкою проблемою, на яку потрібно звертати увагу. Але намагаючись вирішити це, граматику важче читати.
Як зазначено у розділі 2.5 Книги Драконів:
У нас, мабуть, є конфлікт: з одного боку, нам потрібна граматика, яка полегшує переклад, з іншого боку, нам потрібна істотно інша граматика, яка полегшує розбір. Рішення - почати з граматики для легкого перекладу та ретельно перетворити її для полегшення розбору. Видаляючи ліву рекурсію, ми можемо отримати граматику, придатну для використання в перекладачі прогнозного рекурсивного походження.
У парсерів стилю LR немає проблеми цієї лівої рекурсії, оскільки вони будують дерево знизу вгору.
Однак ментальний переклад граматики на зразок вище на аналізатор стилю LR (який часто реалізовується як Кінцевий стан Автоматики ) зробити
дуже важко (і схильний до помилок), оскільки часто існують сотні чи тисячі станів + державні переходи для розгляду. Ось чому парсери у стилі LR зазвичай генеруються генератором парсера, який також відомий як "компілятор компілятора".
Як вирішити двозначності
Ми побачили два способи розв'язання ліво-рекурсійних амбіцій вище: 1) переписати синтаксис 2) використовувати LR-парсер.
Але є й інші види амбіцій, які важче вирішити: Що робити, якщо два різних правила однаково застосовуються одночасно?
Деякі поширені приклади:
Як парсери у стилі LL, так і в LR-стилі, є проблеми з цим. Проблеми з розбором арифметичних виразів можна вирішити, ввівши пріоритет оператора. Аналогічним чином можна вирішити й інші проблеми, такі як Данглінг Ельз, вибравши одну поведінку пріоритету та дотримуючись її. (Наприклад, у C / C ++, звисання ще належить до найближчого "якщо").
Іншим «рішенням» цього є використання граматики вираження Parser Expression Gramatika (PEG): Це схоже на граматику BNF, що використовується вище, але у випадку неоднозначності завжди «вибирайте перше». Звичайно, це насправді не «вирішує» проблему, а скоріше приховує, що двозначність насправді існує: кінцеві користувачі можуть не знати, який вибір робить аналізатор, і це може призвести до несподіваних результатів.
Більше інформації, яка є набагато більш поглибленою, ніж ця публікація, включаючи те, чому взагалі неможливо дізнатися, чи немає у вашій граматиці неоднозначностей і наслідки цього - чудова стаття в блозі LL та LR в контексті: Чому аналіз інструменти важкі . Я настійно рекомендую його; це мені дуже допомогло зрозуміти всі речі, про які я зараз говорю.
50 років досліджень
Але життя продовжується. Виявилося, що "нормальним" парсерам у стилі LR, реалізованим як автоматизовані автоматичні системи, часто потрібні тисячі станів + переходів, що було проблемою в розмірі програми. Так, написані такі варіанти, як Simple LR (SLR) та LALR (Look-forward LR), які поєднують інші методи, щоб зменшити автоматику, зменшивши дисковий і пам’ятний слід парсерів.
Крім того, ще одним способом розв'язання перелічених вище амбіцій є використання узагальнених методів, у яких у разі неоднозначності зберігаються та розбираються обидві можливості: або один може не вдатися до розбору по лінії (у цьому випадку іншою можливістю є "правильний" один), а також повернення обох (і таким чином показує, що існує двозначність) у тому випадку, якщо вони обидва є правильними.
Цікаво, що після опису алгоритму Узагальненого LR виявилося, що подібний підхід може бути використаний для реалізації узагальнених парсерів LL , що є аналогічно швидким ($ O (n ^ 3) $ складність часу для неоднозначних граматик, $ O (n) $ за абсолютно недвозначні граматики, хоча і з більш великим бухгалтерським обліком, ніж простий (ЛА) LR аналізатор, що означає більш високий постійний коефіцієнт), але знову дозволяють парсеру писати в рекурсивному стилі спуску (зверху вниз), що набагато природніше писати та налагоджувати.
Парсер комбінаторів, парсерних генераторів
Отже, з цієї тривалої експозиції ми зараз підходимо до суті питання:
У чому полягає відмінність комбінаторів парсера та генераторів парсера і коли один слід використовувати над іншим?
Вони справді різні звірі:
Комбінатори парсерів були створені тому, що люди писали парсери зверху вниз і зрозуміли, що багато з них мають багато спільного .
Генератори парсерів були створені тому, що люди прагнули створити парсери, які не мали проблем, які мали парсери в стилі LL (тобто парсери в стилі LR), що виявилося дуже важко зробити вручну. До поширених належать Yacc / Bison, які реалізують (LA) LR).
Цікаво, що нині пейзаж дещо затуманений:
Можна записати Parser Combinators, які працюють з алгоритмом GLL , вирішуючи проблеми двозначності, які мали класичні парсери в стилі LL, при цьому настільки ж читабельні / зрозумілі, як і всі види розбору зверху вниз.
Генератори парсера також можуть бути записані для парсерів у стилі LL. ANTLR робить саме це і використовує іншу евристику (Adaptive LL (*)) для вирішення неоднозначностей, які мали класичні аналізатори стилю LL.
Взагалі, створити генератор аналізатора LR та налагодити вихід генератора (LA) LR-стимулятора парсеру, що працює на вашій граматиці, важко через переклад вашої оригінальної граматики у форму LR "всередину". З іншого боку, такі інструменти , як Yacc / Bison було багато років оптимізацій, і бачив багато використання в дикій природі, а це значить , що багато людей тепер вважають це в спосіб зробити синтаксичний і скептично ставляться до нових підходів.
Який з них ви повинні використовувати, залежить від того, наскільки важка граматика вам потрібна, і наскільки швидким повинен бути аналіз. Залежно від граматики, одна з цих методик (/ реалізація різних методик) може бути швидшою, мати менший слід пам’яті, менший слід диска, або бути більш розширюваним або легшим для налагодження, ніж інші. Ваш пробіг може бути різним .
Побічна примітка: На предмет лексичного аналізу.
Лексичний аналіз може використовуватися як для парсерних комбінаторів, так і для парсерних генераторів. Ідея полягає у тому, щоб мати «німий» аналізатор, який дуже легко здійснити (і тому швидко), який виконує перший пропуск над вашим вихідним кодом, видаляючи, наприклад, повторення пробілів, коментарів тощо, і, можливо, дуже токенізуючий грубим способом різні елементи, що складають вашу мову.
Основна перевага полягає в тому, що цей перший крок робить реальний аналізатор набагато простішим (а через це можливо швидшим). Основним недоліком є те, що у вас є окремий крок перекладу, і, наприклад, повідомлення про помилки з номерами рядків і стовпців стає складніше через видалення пробілів.
Зрештою, лексер - це просто «інший аналізатор», який можна реалізувати, використовуючи будь-який із наведених вище методів. Через свою простоту часто використовуються інші методи, ніж для основного аналізатора, і, наприклад, існують додаткові генератори лексерів.
Tl; Dr:
Ось блок-схема, яка застосовується в більшості випадків:
javac
Scala). Це дає вам найбільший контроль над станом внутрішнього парсеру, що допомагає створювати хороші повідомлення про помилки (що останніми роками…