Какая процедура применяется при написании лексера на основе грамматики?


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 Расширенная форма Бэкуса – Наура и форма Бэкуса – Наура

Ответы:


18

Лексеры - это просто простые парсеры, которые используются для оптимизации производительности основного парсера. Если у нас есть лексер, лексер и парсер работают вместе, чтобы описать полный язык. Парсеры, у которых нет отдельной ступени лексирования, иногда называют «без сканера».

Без лексеров синтаксический анализатор должен был бы работать на посимвольной основе. Поскольку синтаксический анализатор должен хранить метаданные о каждом элементе ввода и, возможно, должен предварительно рассчитывать таблицы для каждого состояния элемента ввода, это приведет к недопустимому потреблению памяти для больших размеров ввода. В частности, нам не нужен отдельный узел на символ в абстрактном синтаксическом дереве.

Поскольку текст по буквам является довольно неоднозначным, это также привело бы к гораздо большей неоднозначности, с которой надоедать. Представьте себе правило 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) раз.


Очень хороший ответ @amon, спасибо. Я собираюсь занять некоторое время, чтобы полностью переварить это. Мне, однако, было интересно узнать несколько вещей о вашем ответе. Вокруг восьмого параграфа вы показываете, как я мог бы изменить мою примерную грамматику EBNF в правила для парсера. Будет ли показанная вами грамматика использоваться синтаксическим анализатором? Или есть еще отдельная грамматика для парсера?
Кристиан Дин

@ Инженер Я сделал пару правок. Ваш EBNF может использоваться непосредственно анализатором. Однако мой пример показывает, какие части грамматики могут обрабатываться отдельным лексером. Любые другие правила будут по-прежнему обрабатываться основным анализатором, но в вашем примере это просто input = digit | string.
Амон

4
Большое преимущество синтаксических анализаторов заключается в том, что их гораздо проще создавать; крайним примером этого являются библиотеки комбинаторов синтаксического анализа, где вы ничего не делаете, кроме создания синтаксических анализаторов. Составление синтаксических анализаторов интересно для таких случаев, как ECMAScript-встраиваемый-в-HTML-встраиваемый-в-PHP-разбросанный-с-SQL-с-шаблоном-языком-на-вершине или Ruby-examples-embedded-in-in-Markdown- встроенные в Ruby-документация-комментарии или что-то в этом роде.
Йорг Миттаг

Последний пункт очень важен, но я чувствую, что то, как вы написали, вводит в заблуждение. Это правда, что лексеры не могут быть легко использованы с синтаксисом на основе отступов, но в этом случае анализ без сканирования еще сложнее. Таким образом, вы действительно хотите использовать лексер, если у вас есть такой язык, дополняя его соответствующим состоянием.
user541686

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