Должен ли я использовать лексер при использовании библиотеки комбинатора синтаксического анализатора, такой как Parsec?

При написании синтаксического анализатора в библиотеке комбинаторов синтаксического анализатора, такой как Parsec в Haskell, у вас обычно есть 2 варианта:

  • Напишите лексер для разделения входных данных String на токены, а затем выполните синтаксический анализ [Token]
  • Напрямую напишите комбинаторы синтаксического анализатора на String

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

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

Каковы общие компромиссы между лексированием и бездействием?


person nh2    schedule 05.03.2013    source источник
comment
По моему опыту, это зависит от языка, который вы разбираете. Некоторые языки очень сложно разбирать без предварительного лексирования.   -  person Pubby    schedule 05.03.2013
comment
@Pubby, нет, таких языков нет. Любой язык можно отлично проанализировать без лексера.   -  person SK-logic    schedule 05.03.2013


Ответы (1)


Самая важная разница в том, что при лексировании переводится ваш входной домен.

Хорошим результатом этого является то, что

  • Вам больше не нужно думать о пробелах. В прямом (без лексирования) синтаксическом анализаторе вам нужно добавить space синтаксических анализаторов во все места, где разрешено использование пробелов, о чем легко забыть, и это загромождает ваш код, если пробелы должны разделять все ваши токены. так или иначе.

  • Вы можете думать о своем вкладе по частям, что легко для людей.

Однако если вы все же выполните лексирование, вы получите проблемы, которые

  • Вы больше не можете использовать обычные парсеры на String - например, для синтаксического анализа числа с помощью библиотечной функции parseFloat :: Parsec String s Float (которая работает с входным потоком String) вам нужно сделать что-то вроде takeNextToken :: TokenParser String и execute синтаксического анализатора parseFloat, проверяя результат синтаксического анализа (обычно Either ErrorMessage a). Это запутано в написании и ограничивает возможность компоновки.

  • Вы настроили все сообщения об ошибках. Если ваш синтаксический анализатор токенов не работает на 20-м токене, где это во входной строке? Вам придется вручную сопоставить местоположения ошибок с входной строкой, что утомительно (в Parsec это означает корректировку всех значений SourcePos).

  • Отчеты об ошибках вообще хуже. Запуск string "hello" *> space *> float при неправильном вводе, например "hello4", точно скажет вам, что ожидаемые пробелы отсутствуют после hello, в то время как лексер просто заявит, что нашел "invalid token".

  • Многие вещи, которые, как можно было бы ожидать, будут атомарными единицами и будут разделены лексером, на самом деле "слишком сложно" определить для лексера. Возьмем, к примеру, строковые литералы - вдруг "hello world" больше не являются двумя токенами "hello и world" (но только, конечно, если кавычки не экранированы, как \") - хотя это очень естественно для парсера, это означает сложные правила и особые случаи для лексера.

  • Вы не можете повторно использовать синтаксические анализаторы токенов. Если вы определяете, как разобрать двойник из String, экспортируйте его, и остальной мир сможет его использовать; они не могут сначала запустить ваш (специализированный) токенизатор.

  • Вы застряли в этом. Когда вы разрабатываете язык для синтаксического анализа, использование лексера может привести вас к принятию ранних решений, исправляя то, что вы, возможно, захотите изменить впоследствии. Например, представьте, что вы определили язык, содержащий некоторый токен Float. В какой-то момент вы захотите ввести отрицательные литералы (-3.4 и - 3.4) - это может быть невозможно из-за того, что лексер интерпретирует пробелы как разделители токенов. Используя подход, основанный только на синтаксическом анализаторе, вы можете оставаться более гибкими, упростив внесение изменений в свой язык. В этом нет ничего удивительного, поскольку синтаксический анализатор - более сложный инструмент, который по своей сути кодирует правила.

Подводя итог, я бы рекомендовал писать парсеры без лексера для большинства случаев.

В конце концов, лексер - это просто "упрощенный" * парсер - если вам все равно нужен синтаксический анализатор, объедините их в один.


* Из теории вычислений мы знаем, что все обычные языки также являются контекстно-свободными языками; лексеры обычно обычные, анализаторы контекстно-зависимые или даже контекстно-зависимые (монадические синтаксические анализаторы, такие как Parsec, могут выражать контекстно-зависимые).

person nh2    schedule 05.03.2013
comment
А как насчет производительности? Я думаю, что если вы все равно используете Parsec, производительность не имеет первостепенного значения, но это все же возможное соображение. - person Tikhon Jelvis; 05.03.2013
comment
Другая потенциальная проблема с выделенным лексером заключается в том, что вы больше не сможете реализовать расширяемые парсеры (с разными наборами токенов). - person SK-logic; 05.03.2013
comment
Хороший ответ, но я должен не согласиться с тем, что атомарные единицы сложны в примере лексера. Я, конечно, не специалист в теории, но считаю, что строки с разделителями могут быть довольно легко проанализированы с помощью обычной грамматики. т.е. /^"([^\\"]|\\")*"/ - это реальное регулярное выражение (в формальном смысле - я думаю), которое даже имеет дело с экранированием. Тем не менее, суть хорошо понята. - person Matt Fenwick; 05.03.2013
comment
Согласен, здесь стоит отметить производительность. Я перешел из стиля all-parser в parser + lexer (оба в Parsec), затем на parser + alex-generated-lexer, и каждый шаг заметно увеличивал производительность. - person ScottWest; 14.03.2013
comment
Еще одним приятным результатом использования сканера с регулярными выражениями является то, что эти выражения могут автоматически учитываться при преобразовании NFA в DFA. Хотя в Parsec это можно сделать вручную, на практике все вместо этого прибегают к обратному отслеживанию, что менее эффективно как во времени, так и в пространстве. - person John F. Miller; 22.04.2013