Лексеры - это просто простые парсеры, которые используются для оптимизации производительности основного парсера. Если у нас есть лексер, лексер и парсер работают вместе, чтобы описать полный язык. Парсеры, у которых нет отдельной ступени лексирования, иногда называют «без сканера».
Без лексеров синтаксический анализатор должен был бы работать на посимвольной основе. Поскольку синтаксический анализатор должен хранить метаданные о каждом элементе ввода и, возможно, должен предварительно рассчитывать таблицы для каждого состояния элемента ввода, это приведет к недопустимому потреблению памяти для больших размеров ввода. В частности, нам не нужен отдельный узел на символ в абстрактном синтаксическом дереве.
Поскольку текст по буквам является довольно неоднозначным, это также привело бы к гораздо большей неоднозначности, с которой надоедать. Представьте себе правило R → identifier | "for " identifier
. где идентификатор состоит из букв ASCII. Если я хочу избежать двусмысленности, мне теперь нужно посмотреть на 4 символа, чтобы определить, какую альтернативу следует выбрать. С лексером парсер просто должен проверить, имеет ли он токен IDENTIFIER или FOR - просмотр с 1 токеном.
Двухуровневые грамматики.
Лексеры работают путем перевода входного алфавита в более удобный алфавит.
Сканер без сканирования описывает грамматику (N, Σ, P, S), где нетерминалы N - это левые части правил в грамматике, алфавит Σ - это, например, символы ASCII, произведения P - это правила в грамматике. и начальный символ S - это правило верхнего уровня парсера.
Теперь лексер определяет алфавит токенов a, b, c,…. Это позволяет главному анализатору использовать эти токены в качестве алфавита: Σ = {a, b, c,…}. Для лексера эти токены не являются терминалами, и правилом запуска S L является S L → ε | S | б S | с 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
. Конечно, выражение может содержать другие строки, что приводит нас к следующей проблеме.
Когда токены могут содержать друг друга. В C-подобных языках ключевые слова неотличимы от идентификаторов. Это решается в лексере путем расстановки приоритетов ключевых слов над идентификаторами. Такая стратегия не всегда возможна. Представьте себе файл конфигурации, где 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) раз.
input = digit | string
.