LL-аналізатор

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

LL-аналізатор — алгоритм синтаксичного аналізу методом рекурсивного спуску для підмножини контекстно-вільних граматик. Він обробляє вхід зліва направо (тому перша буква означає Left) та будує ліворекурсивне виведення рядка (тому його порівнюють з LR-аналізатором). Клас граматик, що розпізнаються цим аналізатором, називається LL-граматиками.

Далі описується табличний аналізатор — альтернатива алгоритму рекурсивного спуску, який зазвичай кодується вручну (хоча не завжди, дивіться наприклад ANTLR для генератора аналізаторів LL(*)-граматик методом рекурсивного спуску).

LL-аналізатори називаються LL(k)-аналізаторами, якщо вони дивляться на k токенів вперед протягом аналізу виразу. Якщо такий аналізатор існує та може розпізнавати вирази граматики без бектрекінгу, тоді граматика називається LL(k)-граматикою. З цих граматик найпопулярнішою граматикою є граматика LL(1), бо незважаючи на її обмеженість, вона має дуже простий аналізатор. Мови, що відповідають LL(k)-граматикам з великим k, вважаються такими, що важко аналізуються, хоча сьогодні це не зовсім вірно через доступність та поширеність генераторів синтаксичних аналізаторів, що підтримують LL(k)-граматики.

LL-аналізатор називається LL(*)-аналізатором, якщо він не обмежений скінченним числом токенів для попереднього перегляду, а може приймати рішення визначаючи чи належать вхідні токени регулярній мові (наприклад використовуючи ДСкА).

Існує конкуренція між «європейською школою» проектування мов, яка віддає перевагу LL-граматикам, та «американською», яка частіше використовує LR-граматики. Це багато в чому завдяки традиціям викладання та детальному опису методів та інструментів в літературі. Інший вплив іде від досліджень Ніклауса Вірта в Вищій технічній школі Цюріха, які описували багато способів оптимізації LL(1)-мов та компіляторів.

Загальний випадок

[ред. | ред. код]

Аналізатор працює на рядках з певної контекстно вільної граматики.

Аналізатор складається з

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

Аналізатор застосовує правило з таблиці, яке відповідає символу на вершині стеку (рядок таблиці) та символу з вхідного потоку (стовпець).

Коли аналізатор запускається, стек вже містить два символи:

[ S, $ ]

де '$' — спеціальний термінал, що вказує на дно стека, та кінець вхідного потоку, а 'S' — аксіома граматики. Аналізатор спробує змінити вміст стека на те, що він знайде на вході. Тим не менш він тільки тримає в стеку те, що має бути переписаним.

Конкретний приклад

[ред. | ред. код]

Ініціалізація

[ред. | ред. код]

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

  1. S → F
  2. S → ( S + F )
  3. F → a

яка аналізує наступний вхід:

( a + a )

Таблиця аналізу для цієї граматики виглядає так:

( ) a + $
S 2 1
F 3

(Зауважте, що є також стовпчик для спеціального терміналу $, що позначає кінець вводу.)

Процедура аналізу

[ред. | ред. код]

На кожному кроці парсер читає наступний доступний символ з вхідного потоку, та символ на вершині стеку. Якщо вхідний символ та символ на вершині стеку співпадають, парсер відкидає їх обох, залишаючи тільки символи, що не співпали.

Тому на першому кроці аналізатор читає вхідний символ '(' та символ на вершині стеку 'S'. Інструкції з таблиці аналізу приходять від колонки з заголовком '(' та рядком з заголовком 'S'; ця клітинка містить '2', що вказує аналізатору застосувати правило (2). Аналізатор має замінити 'S' на '( S + F )' на вершині стеку та вивести правило номер 2. Стек приймає вигляд:

[ (, S, +, F, ), $ ]

Так як '(' з вхідного потоку не співпадає з символом на вершині стеку, 'S', він не видаляється, та залишається як наступний доступний вхідний символ, для наступного кроку.

На другому кроці аналізатор видаляє ( з вхідного потоку, та з стеку, так як вони співпадають. Стек стає:

[ S, +, F, ), $ ]

Тепер на вході 'a' та 'S' у стеку. Таблиця каже застосувати правило (1) граматики та вивести правило (1) у вихідний потік. Стек стає:

[ F, +, F, ), $ ]

Аналізатор має 'a' на вході, та 'F' на вершині стеку. Таблиця каже застосувати правило (3). Стек стає:

[ a, +, F, ), $ ]

На наступних двох кроках аналізатор читає 'a' та '+' з вхідного потоку, та, так як вони співпадають з символами в стеку теж видаляє їх звідти. Результат:

[ F, ), $ ]

На наступних трьох кроках аналізатор замінить 'F' зі стеку на 'a, виведе правило 3 та видалить 'a' та ')' зі стеку та з вхідного рядка. Аналізатор таким чином завершить роботу коли '$' міститиметься як на вході, так і у стеку.

В такому випадку аналізатор скаже що він приймає вхідний рядок, та виведе наступний список номерів правил у вихідний потік:

[ 2, 1, 3, 3 ]

Це насправді список правил для ліворекурсивного виведення вхідного рядка у даній контекстно-вільній граматиці, які утворюють такий ланцюжок:

S → ( S + F )( F + F )( a + F )( a + a )

Реалізація аналізатора на C++

[ред. | ред. код]

Нижче дана реалізація на C++ табличного LL-аналізатора для мови, що була дана в прикладі:

Примітки

[ред. | ред. код]

Як можна було бачити на прикладі, аналізатор виконує три види кроків залежно від того чи на вершині стеку термінал, нетермінал, чи спеціальний символ $:

  • Якщо на вершині нетермінал, тоді він шукає в таблиці аналізу за базисом нетерміналу та символом у вхідному потоці яке граматичне правило варто використати, щоб замінити його у стеку. Номер правила записується у вихідний потік. Якщо таблиця каже що потрібного правила не існує, то виводиться помилка, і робота припиняється.
  • Якщо на вершині термінал, тоді він порівнюється з символом на вході, та якщо вони рівні, вони обидва видаляються. Якщо вони не рівні, аналізатор виводить помилку та зупиняється.
  • Якщо на вершині стеку $ та у вхідному потоці теж, тоді аналізатор доповідає про успішний аналіз вхідного потоку, інакше виводить помилку. В обох випадках аналіз зупиняється.

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

Побудова таблиці LL(1)-аналізатора

[ред. | ред. код]

Щоб заповнити таблицю аналізу, ми маємо з'ясувати яке правило використовувати, коли аналізатор бачить нетермінал A на вершині стеку та символ a на вході. Легко побачити що це правило має мати форму A -> w та що мова що відповідає w має мати хоч одне слово, що починається з w. З такою метою ми описуємо First-set для w що записується тут як Fi(w), як множину терміналів, що можуть знаходитись на початку будь-якого слова з w, плюс ε якщо порожнє слово теж належить w. Маючи граматику з правилами A1w1, …, Anwn, ми можемо обчислити Fi(wi) та Fi(Ai) для кожного правила так:

  1. ініціалізуємо кожне Fi(wi) та Fi(Ai) порожньою множиною.
  2. додаємо Fi(wi) до Fi(Ai) для кожного правила Aiwi, де Fi описується так:
    • Fi(a w' ) = { a } для кожного теміналу a
    • Fi(A w' ) = Fi(A) для кожного такого нетерміналу A, що ε не належить Fi(A)
    • Fi(A w' ) = Fi(A) \ { ε } ∪ Fi(w' ) для кожного нетерміналу A, що ε належить Fi(A)
    • Fi(ε) = { ε }
  3. додаємо Fi(wi) до Fi(Ai) для кожного правила Aiwi
  4. повторюємо кроки 2 та 3, аж поки всі множини Fi не стануть однаковими.

На жаль, множин Fi не достатньо, щоб вирахувати таблицю аналізу. Це тому, що права частина правила w може бути переписана в порожній рядок. Тому аналізатор також має використовувати правило A -> w якщо ε належить Fi(w) та видимий у вхідному потоці, як символ, що може йти за A. Тому ми також потребуємо множину Follow для A, яку запишемо як Fo(A) , яка описується як множина терміналів a таких що існує рядок символів αAaβ які можуть виводитись з початкового символу. Обчислення множини Follow для нетерміналів в граматиці може бути здійснене так:

  1. ініціалізувати кожне Fo(Ai) порожньою множиною
  2. якщо існує правило виду AjwAiw' , тоді
    • якщо термінал a належить Fi(w' ), то додати a до Fo(Ai)
    • якщо ε належить Fi(w' ), то додати Fo(Aj) до Fo(Ai)
  3. повторювати крок 2 поки всі Fo не стануть однаковими.

Тепер ми можемо описати точно, які правила де будуть зберігатись в таблиці аналізу. Якщо T[A, a] позначає місце в таблиці для нетерміналу A та терміналу a, тоді

T[A,a] містить правило Aw тоді і тільки тоді коли
a належить Fi(w) або
ε належить Fi(w) і a належить Fo(A).

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

Побудова таблиці аналізу для LL(k)-граматики

[ред. | ред. код]

До середини 1990-тих вважалось, що LL(k)-аналіз (для k > 1) є непрактичним, так як розмір таблиці аналізу буде (зазвичай в гіршому випадку) мати експоненційну складність від k. Таке сприйняття сильно змінилось з випуском PCCTS близько 1992, коли було показано, що багато мов програмування можуть ефективно аналізуватись синтаксичним аналізатором LL(k)-граматики без використання поведінки для гіршого випадку. Більше того, в деяких випадках LL-аналіз можливий навіть для необмеженого преперегляду. А традиційні генератори аналізаторів, як yacc використовують таблиці аналізу LALR(1) щоб створити обмежений LR-аналізатор з фіксованим, однотокеневим препереглядом.

Конфлікти

[ред. | ред. код]

Конфлікти LL(1)

[ред. | ред. код]

Є три типи LL(1)-конфліктів:

  • FIRST/FIRST
Множини FIRST двох різних нетерміналів перетинаються.
  • FIRST/FOLLOW
Множини правил граматики FIRST та FOLLOW перетинаються. З ε, що належить множині FIRST, невідомо яку альтернативу вибрати.
Приклад конфліктів LL(1) :
 S -> A 'a' 'b'
 A -> 'a' | ε
Множина FIRST для A дорівнює { 'a' ε } та множина FOLLOW { 'a' }.
  • ліво-рекурсивний
Ліва рекурсія спричинить конфлікт FIRST/FIRST з усіма альтернативами.
 E -> E '+' term | alt1 | alt2

Розв'язання конфліктів LL(1)

[ред. | ред. код]
  • Лівостороннє винесення за дужки

Працює приблизно як 3x + 3y = 3(x+y).

 A -> X | X Y Z

стає

 A -> X B
 B -> Y Z | ε

Може застосовуватись коли дві альтернативи починаються з одного і того ж символу, як у конфлікті FIRST/FIRST.

  • Підстановка

Підстановка правила в інше правило, щоб вилучити непрямі конфлікти, чи конфлікти FIRST/FOLLOW. Зауважте, що це може спричинити конфлікт FIRST/FIRST.

  • Вилучення лівої рекурсії[1]

Простий приклад вилучення лівої рекурсії:

Наступні продуктивні правила мають ліву рекурсію на E

 E -> E '+' T
   -> T

Ці правила задають ніщо інше, окрім списку з T, розділених '+'. У формі регулярного виразу записується так: T ('+' T)*. Тому правила можна переписати так

 E -> T Z
 Z -> '+' T Z
   -> ε

Тепер немає лівої рекурсії та конфліктів у жодному з правил.

Див. також

[ред. | ред. код]

Генератори синтаксичних аналізаторів:

Посилання

[ред. | ред. код]

Джерела

[ред. | ред. код]
  1. Modern Compiler Design, Grune, Bal, Jacobs and Langendoen