Лексери - це просто прості парсери, які використовуються як оптимізація продуктивності для основного аналізатора. Якщо у нас є лексери, лексери та аналізатори працюють разом, щоб описати повну мову. Парсери, які не мають окремої стадії лексингу, іноді називають "сканером".
Без лексерів аналізатору доведеться діяти на основі персонажів. Оскільки аналізатору належить зберігати метадані про кожен елемент введення, і, можливо, доведеться попередньо обчислювати таблиці для кожного стану вхідного елемента, це призведе до неприйнятного споживання пам'яті для великих розмірів вводу. Зокрема, нам не потрібно окремого вузла на кожного символу в абстрактному синтаксичному дереві.
Оскільки текст на основі символу за символом є досить неоднозначним, це також призведе до набагато більшої неоднозначності, яка дратує обробляти. Уявіть собі правило 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) час.