Яка процедура дотримується під час написання лексики на основі граматики?


13

Читаючи відповідь на запитання Роз’яснення щодо граматики, лексерів та парсерів , у відповіді було зазначено, що:

[...] Граматика BNF містить усі правила, необхідні для лексичного аналізу та розбору.

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

Якщо лексери, як і парсери, базуються на граматиці EBNF / BNF, то як би ви могли створити лексеру за допомогою цього методу? Тобто, як би я сконструював лексеми, використовуючи задану граматику EBNF / BNF?

Я бачив багато, багато публікацій, які займаються написанням аналізатора, використовуючи EBNF / BNF як керівництво або креслення, але я досі не зустрічав жодної, яка б показала еквівалент лексерового дизайну.

Наприклад, візьміть таку граматику:

input = digit| string ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"', { all characters - '"' }, '"' ;
all characters = ? all visible characters ? ;

Як можна створити лексику, засновану на граматиці? Я міг би уявити, як з такої граматики можна записати аналізатор, але я не можу зрозуміти поняття зробити те саме з лексемою.

Чи існують певні правила або логіка, яка використовується для виконання такого завдання, як написання аналізатора? Відверто кажучи, я починаю замислюватися про те, чи лексерові конструкції взагалі використовують граматику EBNF / BNF, не кажучи вже про основу на одній.


1 Розширена форма Backus – Naur та форма Backus – Naur

Відповіді:


18

Лексери - це просто прості парсери, які використовуються як оптимізація продуктивності для основного аналізатора. Якщо у нас є лексери, лексери та аналізатори працюють разом, щоб описати повну мову. Парсери, які не мають окремої стадії лексингу, іноді називають "сканером".

Без лексерів аналізатору доведеться діяти на основі персонажів. Оскільки аналізатору належить зберігати метадані про кожен елемент введення, і, можливо, доведеться попередньо обчислювати таблиці для кожного стану вхідного елемента, це призведе до неприйнятного споживання пам'яті для великих розмірів вводу. Зокрема, нам не потрібно окремого вузла на кожного символу в абстрактному синтаксичному дереві.

Оскільки текст на основі символу за символом є досить неоднозначним, це також призведе до набагато більшої неоднозначності, яка дратує обробляти. Уявіть собі правило R → identifier | "for " identifier. де ідентифікатор складається з літер ASCII. Якщо я хочу уникнути двозначності, мені зараз потрібен чотиризначний пошук, щоб визначити, яку альтернативу слід вибрати. За допомогою лексеру аналізатор повинен просто перевірити, чи має він маркер IDENTIFIER або FOR - 1-лексему.

Дворівневі граматики.

Лексери працюють, перекладаючи вхідний алфавіт на більш зручний алфавіт.

Сканер без сканера описує граматику (N, Σ, P, S), де нетермінали N - ліва сторона правил у граматиці, алфавіт Σ - це наприклад символи ASCII, виробництво P - правила граматики , а символ запуску S - правило верхнього рівня парсера.

Тепер лексема визначає алфавіт лексем a, b, c,…. Це дозволяє основному парсеру використовувати ці лексеми як алфавіт: Σ = {a, b, c,…}. Для лексеру ці лексеми є нетермінальними, а правило запуску S L є S L → ε | a S | b S | c S | …, Тобто будь-яка послідовність лексем. Правила граматики лексеру - це всі правила, необхідні для створення цих лексем.

Перевага у виконанні полягає у вираженні правил лексеми як звичайній мові . Їх можна проаналізувати набагато ефективніше, ніж контекстні мови. Зокрема, звичайні мови можна розпізнати в просторі O (n) та O (n) часу. На практиці генератор коду може перетворити такий лексер у високоефективні таблиці стрибків.

Вилучення лексем із граматики.

Щоб торкнутися вашого прикладу: digitі stringправила виражаються на рівні символів за символом. Ми могли б використовувати їх як лексеми. Решта граматики залишається недоторканою. Ось граматика лексеми, написана як праволінійна граматика, щоб зрозуміти, що це регулярно:

digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;

Але оскільки це регулярно, ми зазвичай використовуємо регулярні вирази для вираження синтаксису лексеми. Ось наведені вище визначення лексем як регулярні вирази, написані за допомогою синтаксису виключення класів .NET символів та часових класів POSIX:

digit ~ [0-9]
string ~ "[[:print:]-["]]*"

Граматика основного аналізатора містить решта правил, якими не користується лексер. У вашому випадку це просто:

input = digit | string ;

Коли лексери не можна легко використовувати.

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

  • Під час вставки мов. Деякі мови дозволяють інтерполювати код в рядки: "name={expression}". Синтаксис виразів є частиною без контекстної граматики і тому не може бути токенізований регулярним виразом. Щоб вирішити це, ми або рекомбінуємо парсер з лексером, або вводимо додаткові лексеми на кшталт STRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END. Правило граматики для рядка може виглядати так: String → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END. Звичайно, вираз може містити інші рядки, що призводить нас до наступної проблеми.

  • Коли жетони могли містити один одного. У мовах, подібних С, ключові слова не відрізняються від ідентифікаторів. Це вирішується в лексемі шляхом визначення пріоритетності ключових слів над ідентифікаторами. Така стратегія не завжди можлива. Уявіть файл конфігурації, де Line → IDENTIFIER " = " REST, де решта - будь-який символ до кінця рядка, навіть якщо решта виглядає як ідентифікатор. Приклад рядка буде a = b c. Лексер насправді німий і не знає, в якому порядку можуть з’являтися лексеми. Отже, якщо ми поставимо пріоритет IDENTIFIER перед REST, лексер дасть нам IDENT(a), " = ", IDENT(b), REST( c). Якщо ми поставимо пріоритет REST над IDENTIFIER, лексер просто дасть нам REST(a = b c).

    Щоб вирішити це, ми повинні рекомбінувати лексери з аналізатором. Розділення можна дещо підтримати, зробивши лексеру ледачим: кожен раз, коли аналізатору потрібен наступний маркер, він вимагає його від лексеру і повідомляє лексему набір прийнятних лексем. Ефективно ми створюємо нове правило верхнього рівня для граматики лексерів для кожної позиції. Тут це призведе до дзвінків nextToken(IDENT), nextToken(" = "), nextToken(REST), і все працює добре. Для цього потрібен аналізатор, який знає повний набір прийнятних маркерів у кожному місці, що передбачає аналізатор знизу вгору, як LR.

  • Коли лексеру доводиться підтримувати стан. Наприклад, мова Python розмежовує кодові блоки не фігурними дужками, а відступом. Існують способи поводження з синтаксисом, чутливим до компонування в межах граматики, але ці методи є надмірними для Python. Натомість лексема перевіряє відступ кожного рядка та випускає маркери INDENT, якщо знайдено новий відступний блок, і DEDENT лексеми, якщо блок закінчився. Це спрощує основну граматику, оскільки тепер вона може робити вигляд, що лексеми є як фігурні дужки. Однак лексеру потрібно підтримувати стан: поточний відступ. Це означає, що лексера технічно вже не описує звичайну мову, а фактично контекстну мову. На щастя, ця різниця не є актуальною на практиці, і лексер Python все ще може працювати в O (n) час.


Дуже приємна відповідь @amon, дякую. Мені доведеться зайняти деякий час, щоб повністю перетравити його. Я, однак, цікавився кількох речей щодо вашої відповіді. Приблизно у восьмому абзаці ви показуєте, як я міг змінити свій приклад граматики EBNF в правила для розбору. Чи використала б граматика, яку ви показали, і аналізатор? Або ще є окрема граматика для аналізатора?
Крістіан Дін

@Engineer Я зробив декілька змін. Ваш EBNF може бути використаний безпосередньо аналізатором. Однак мій приклад показує, які частини граматики можуть оброблятися окремим лексером. З будь-якими іншими правилами все одно керуватиметься головним аналізатором, але у вашому прикладі це просто input = digit | string.
амон

4
Велика перевага аналізаторів без сканер полягає в тому, що їх набагато простіше складати; крайнім прикладом цього є бібліотеки комбінаторів парсера, де ви нічого не робите, окрім як складати парсери. Композиція парсерів цікава для таких випадків, як ECMAScript-вбудований-у-HTML-вбудований-у-PHP-посипаний-з-SQL-з-шаблоном-мовою-зверху чи Ruby-приклад-вбудований-у-Markdown- вбудована в Ruby-документація-коментарі або щось подібне.
Йорг W Міттаг

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

@Mehrdad Python-стиль лексеми з відступом / виділеними лексемами можливі лише для дуже простих мов, чутливих до відступу, і, як правило, не застосовуються. Більш загальною альтернативою є атрибутичні граматики, але їхньої підтримки не вистачає у стандартних інструментах. Ідея полягає в тому, що ми коментуємо кожен фрагмент AST його відступом і додаємо обмеження до всіх правил. Атрибути легко додати за допомогою комбінаторного розбору, що також полегшує аналіз сканерів без сканерів.
Амон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.