- •Глава 4. Синтаксичний аналіз
- •4.1. Граматики та роль синтаксичного аналізатора
- •4.2. Контекстно-вільні граматики
- •Угоди по позначеннях
- •4.3. Дерева розбору й приведення
- •4.4. Регулярні вирази й контекстно-вільні граматики
- •4.5. Перетворення граматик для спрощення синтаксичного аналізу
- •4.5.1 Усунення неоднозначності
- •4.5.2 Усунення лівої рекурсії
- •4.5.3 Ліва факторизація
4.2. Контекстно-вільні граматики
Багато мов програмування по своїй природі мають рекурсивну структуру, що може визначатися контекстно-вільними граматиками. Наприклад, у нас може бути умовна інструкція, обумовлена наступним правилом.
Якщо S1 й S2 є інструкціями, а Е — виразом, то
if E then S1 else S2 (4.1)
є інструкцією.
Цей тип умовних інструкцій не може бути визначений з використанням регулярних виразів. У главі 2 "Лексичний аналіз", ми бачили, що регулярні вирази можуть визначати лексичну структуру токенів. Однак, використовуючи для позначення класу інструкцій синтаксичну змінну stmt, а для класу виразів — ехрr, можемо легко представити інструкцію (4.1) за допомогою продукції граматики
stmt := if expr then stmt else stmt (4.2)
У цьому розділі розглянемо визначення контекстно-вільної граматики й познайомимося з термінологією, використовуваної при описі процесу синтаксичного розбору. З п. 4.1 ми знаємо, що контекстно-вільна граматика (або, для стислості, просто граматика) складається з терміналів, нетерміналів, стартового символу й продукцій.
Термінали являють собою базові символи, з яких формуються рядки. Слово "токен" є синонімом слова "термінал", коли ми говоримо про граматики мов програмування. В (4.2) кожне із ключових слів if, then, else є терміналом.
Нетермінали являють собою синтаксичні змінні, які визначають множини рядків. В (4.2) stmt й expr є нетерміналами. Нетермінали визначають множини рядків, які допомагають у визначенні мови, породжуваною граматикою. Крім того, вони накладають на мову ієрархічну структуру, що полегшує синтаксичний аналіз і трансляцію.
Один з нетерміналів граматики вважається стартовим символом, і множина рядків, які він визначає, є мовою, обумовленою граматикою.
Продукції граматики визначають спосіб, яким термінали й нетермінали можуть поєднуватися для створення рядків. Кожна продукція складається з нетермінала, за яким слідує три символи ::=, і рядок нетерміналів та/або терміналів.
Приклад 4.3. Граматика з наступними продукціями визначає прості арифметичні вирази:
expr ::= expr op expr
expr ::= (expr)
expr ::= - expr
expr ::= id
op ::= +
op ::= -
op ::= *
op ::= /
У цій граматиці термінальними символами є: id, ‘+’, ‘-‘, ‘*’, ‘/’, ‘(‘, ‘)’, а нетермінальними символами граматики: expr й op. Стартовим символами граматики є expr.
Угоди по позначеннях
Для того щоб уникнути постійного згадування "це — термінал", "це — нетермінал" і т.п., в подальшому будемо використати наступні угоди щодо позначень.
1. Терміналами є символи:
малі літери з початку алфавіту, такі як а, b, c;
символи операторів, такі як +,- і т.п.;
символи пунктуації, такі як коми, дужки й т.п.;
цифри 0, 1, ...., 9;
рядки, виділені жирним шрифтом, такі як id або if.
2. Ці символи є нетерміналами:
прописні букви з початку алфавіту, такі як А, В, C;
буква S, що звичайно означає стартовий символ;
імена з малих літер, виділені курсивом, такі як stmt або expr.
Прописні букви з кінця алфавіту, такі як X, Y, Z, представляють граматичні символи, тобто або термінали, або нетермінали.
Малі літери з кінця алфавіту, такі як u, v, ..., z, позначають рядки терміналів.
Малі грецькі букви, такі як α, β, γ представляють рядки граматичних символів. Таким чином, у загальному виді продукція може бути записана як А ::= α, у якій одиночний нетермінал А розташовується ліворуч від сполучення символів ‘::=’ (у лівій частині продукції), а рядок граматичних символів α у правій частині продукції.
6. Якщо А ::= α1, A ::= α2, ..., A ::= αk являють собою продукції з А в лівій частини (ми називаємо їх А-продукціями), то можемо записати
А ::= α1|α2|...|αk. Ми називаємо α1, α2, ..., αk альтернативами А.
7. Якщо інше не зазначено явно, ліва частина першої продукції є стартовим символом.
Приклад 4.4. Використовуючи наведені угоди, можна записати граматику із приклада 4.2 як
Е ::= ЕАЕ|(Е)|-Е|id
А ::= + | - | * | /
З наших угод слідує, що Е и А — нетермінали, причому Е є стартовим символом. Інші символи являють собою термінали.
Породження
Існує кілька способів розглядати процес визначення мови граматикою. Раніше ми бачили його як процес побудови дерев розбору. Однак є й інший корисний спосіб, що дає точний опис спадної побудови дерева розбору. Основна ідея полягає в тому, що продукція розглядається як правило, що переписує нетермінал з лівої частини і заміщує його рядком із правої частини продукції.
Розглянемо, наприклад, наступну граматику арифметичних виразів із нетерміналом Е, що представляє вираз.
Е ::= Е + Е | Е* Е | (Е) | -Е | id (4.3)
Продукція Е ::= -Е означає, що вираз, якому передує знак мінуса, також є виразом. Ця продукція може використатися для породження (derivation) більш складних виразів із простих, дозволяючи нам заміняти будь-який екземпляр Е на -Е. У найпростішому випадку ми можемо замістити одне Е на -Е и описати це як Е → -Е, що можна прочитати як "Е породжує -Е", або "з Е виводиться -Е", Продукція Е ::= (Е) говорить, що ми можемо також замінити один екземпляр Е в будь-якому рядку граматики на (Е), наприклад Е*Е → (Е)*Е або Е*Е → Е *( Е).
Ми можемо взяти нетермінал Е и неодноразово застосовувати продукції в довільному порядку для одержання послідовності заміщень, наприклад, Е→ -Е → -(Е) → -(id). Таку послідовність замін назвемо висновком (derivation), або породженням -(id) з Е. Такий висновок є доказом того, що одним із прикладів виразу Е є рядок -(id).
Більш абстрактно, ми говоримо, що αAβ → αγβ, якщо А ::= γ є продукцією, а α і β являють собою довільні рядки граматичних символів. Якщо α1 → α2 → ... → αn, говоримо, що α1 породжує αn. Символ → означає "породжує за один крок". Часто ми говоримо "породжує за нуль або більше кроків" і позначаємо це символом →* . Таким чином,
α →* α для будь-якого рядка α,
якщо α →* β и β→* γ , то α →* γ .
Аналогічно символ →+ використається для позначення "породжує за один або більше кроків".
Маючи граматику G зі стартовим символом S, можна використати відношення →* для визначення L(G), мови, породженою граматикою G. Рядки в L(G) можуть містити тільки термінальні символи з G, тобто рядок терміналів w належить L(G) тоді й тільки тоді, коли S →+ w. Рядок w при цьому називається реченням G, а мова, що може бути породжена граматикою, — контекстно-вільною. Якщо дві граматики породжують одну й туж саму мову, то ці граматики називаються еквівалентними.
Якщо S →+ α , де α може містити нетермінали, то ми говоримо про те, що α є сентенціальною формою G. Речення являє собою сентенціальну форму без нетерміналів.
Приклад 4.5
Рядок -(id+id) є реченням граматики (4.3) через наявність породження E→ -E → -(E) → -( E + E) → -(id + E) → -(id + id) (4.4)
Рядки E, -Е, -(E),.., -(id+id), що з'являються в процесі породження, являють собою сентенціальні форми даної граматики. Для підкреслення того, що -(id + id) може бути виведене з Е, ми записуємо E →* -( id + id).
За допомогою індукції по довжині породження можна показати, що кожне речення в мові граматики (4.3) являє собою арифметичний вираз, що включає бінарні оператори * й +, унарний оператор -, дужки й операнд id.
Примітка. Слід розрізняти знаки арифметичних операторів множення ‘*’ й додавання ‘+’ від використання цих же символів як “верхніх індексів’ ‘*’ ‘+’ при символі ‘→’, тлумачення яких було наведено вище.
Аналогічно за допомогою індукції по довжині арифметичного виразу можна показати, що всі такі вирази можуть бути породжені даною граматикою. Отже, граматика (4.3) генерує в точності множину всіх арифметичних виразів, які включають бінарні оператори * й +, унарний оператор -, дужки й операнд id.
На кожному кроці породження здійснюється два вибори - ми повинні вибрати замінний нетермінал, а потім його альтернативу. Наприклад, породження (4.4) із приклада 4.5 може продовжитися після -(E + E) у такий спосіб:
-(E + E) → -(E +id) → -(id+id) (4.5)
Кожен нетермінал в (4.5) заміщується тією же правою частиною, що й в (4.4), але в іншому порядку.
Щоб зрозуміти, як працюють деякі синтаксичні аналізатори, необхідно розглянути породження, у кожній сентенціальній формі яких заміщується самий лівий нетермінал (такі породження називаються лівими – leftmost). Якщо α → β представляє собою породження, у якому замішається крайній лівий нетермінал в α; ми записуємо α lm→ β. Оскільки породження (4.4) є лівим, можемо записати його як
Е lm→ -Е lm→ -(Е) lm→ -( Е + Е) lm→ -(id + Е) lm→ -(id + id)
Використовуючи уведені нами позначення, кожен крок лівого породження можна записати як wAγ lm→ wδγ, де w складається тільки з терміналів, А ::= δ являє собою використану в кроці продукцію, а γ — рядок синтаксичних символів граматики. Для підкреслення того факту, що α породжує β лівим породженням, ми записуємо α lm→* β. Якщо S lm→* α, то говоримо, що α. є лівосентенціальною формою розглянутої граматики.
Аналогічні визначення існують для правих (rightmost) породжень, у яких на кожному кроці заміщається крайній справа нетермінал. Іноді праві приведення називаються канонічними.
