Каждый раз, когда я пишу простой лексер и парсер, я натыкаюсь на один и тот же вопрос: как должен взаимодействовать лексер и парсер? Я вижу четыре разных подхода:
-
Лексер с нетерпением преобразует всю входную строку в вектор токенов. Как только это будет сделано, вектор будет передан в парсер, который преобразует его в дерево. Это, безусловно, самое простое решение для реализации, но поскольку все маркеры хранятся в памяти, он тратит много места.
-
Каждый раз, когда lexer находит маркер, он вызывает функцию в синтаксическом анализаторе, передавая текущий токен. По моему опыту, это работает только в том случае, если парсер, естественно, может быть реализован как конечный автомат, такой как парсер LALR. В отличие от этого, я не думаю, что это будет работать для рекурсивных парсеров спуска.
-
Каждый раз, когда парсеру нужен токен, он запрашивает лексер для следующего. Это очень легко реализовать на С# из-за ключевого слова
yield
, но довольно сложно в С++, который его не имеет. -
Лексер и парсер взаимодействуют через асинхронную очередь. Это широко известно под названием "производитель/потребитель", и это должно упростить общение между лексером и парсером. Превосходит ли он и другие решения на многоядерных процессорах? Или лексирование слишком тривиально?
Является ли мой анализ звуком? Есть ли другие подходы, о которых я не думал? Что используется в реальных компиляторах? Было бы здорово, если бы авторы-компиляторы, такие как Эрик Липперт, могли пролить свет на эту проблему.