Связь между лексер и парсер

каждый раз, когда я пишу простой лексер и парсер, я натыкаюсь на один и тот же вопрос: как лексер и парсер общаться? Я вижу четыре разных подхода:--2-->

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

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

  3. каждый раз, когда парсеру нужен токен, он запрашивает лексер для следующего. Это очень легко реализовать на C# из-за yield ключевое слово, но довольно сложно в C++, который его не имеет.

  4. лексер и парсер взаимодействуют через асинхронную очередь. Это широко известно под названием "производитель / потребитель", и это должно значительно упростить связь между лексером и парсером. Он также превосходит другие решения на multicores? Или лексинг слишком тривиален?

мой анализ звука? Есть ли другие подходы, о которых я не подумал? Что используется в реальные компиляторы? Было бы здорово, если бы авторы компиляторов, такие как Эрик Липперт, могли пролить свет на эту проблему.

5 ответов


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

  1. Лексирование всего ввода перед запуском парсера имеет много преимуществ перед другими параметрами. Реализации различаются, но в целом память, необходимая для этой операции, не является проблемой, особенно если учесть тип информации, которую вы хотите иметь доступной для компиляции отчетов ошибки.

    • льготы
      • потенциально больше информации, доступной во время отчетов об ошибках.
      • языки, написанные таким образом, что позволяет lexing происходить перед синтаксическим анализом, легче указать и написать компиляторы.
    • недостатки
      • некоторые языки требуют контекстно-зависимых лексеров, которые просто не могут работать до фазы синтаксического анализа.

    язык примечание по реализации: это моя предпочтительная стратегия, поскольку она приводит к разделяемому коду и лучше всего подходит для перевода для реализации IDE для языка.

    примечание по реализации парсера: я экспериментировал с ANTLR v3 относительно накладных расходов памяти с этой стратегией. Цель C использует более 130 байт на маркер, а цель Java использует около 44 байт на маркер. С измененной целью C# я показал, что можно полностью представить токенизированный ввод с помощью только 8 байт на токен, что делает эту стратегию практичной даже для довольно больших исходных файлов.

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

  2. похоже, вы описали "push" версию того, что я обычно вижу, описанную как "pull" parser, как у вас в #3. Мой акцент на работе всегда был на разборе LL, так что это не было действительно вариантом для меня. Я был бы удивлен, если есть преимущества для этого над #3, но не могу их исключить.

  3. наиболее сообщают часть это заявление о c++. Правильное использование итераторов в C++ делает это исключительно хорошо подходит для такого типа поведения.

  4. очередь выглядит как перестановка #3 с посредником. А абстрагируясь независимых операций имеет много преимущества в таких областях, как модульная разработка программного обеспечения, пара лексер/парсер для дистрибутивного продукта очень чувствительна к производительности, и этот тип абстракции удаляет возможность выполнять определенные типы оптимизации структуры данных и компоновки памяти. Я бы рекомендовал использовать Вариант №3.

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

Что касается других вариантов: для компилятора, предназначенного для широкого использования (коммерческого или иного), обычно разработчики выбирают стратегию синтаксического анализа и реализацию, которая обеспечивает лучшее производительность в условиях ограничений целевого языка. Некоторые языки (например, Go) могут быть проанализированы исключительно быстро с помощью простой стратегии синтаксического анализа LR, а использование "более мощной" стратегии синтаксического анализа (читай: ненужные функции) только замедлит процесс. Другие языки (например, C++) чрезвычайно сложны или невозможны для анализа с типичными алгоритмами, поэтому используются более медленные, но более мощные/гибкие Парсеры.


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

  1. "вектор токенов". Это решение может иметь большой объем памяти. Представьте себе компиляцию исходного файла с большим количеством заголовков. Хранение самого токена недостаточно. Сообщение об ошибке должно содержать контекст с именем файла и номером строки. Может случиться, что lexer зависит от парсера. Разумный пример: "> > " - это оператор сдвига или это закрытие 2-х слоев экземпляров шаблона? Я бы проголосовал за этот вариант.

  2. (2,3). "Одна часть зовет другую". У меня сложилось впечатление, что более сложную систему следует назвать менее сложной. Я считаю, что лексер проще. Это означает, что парсер должен вызвать lexer. Я не вижу, почему C# лучше чем c++. Я реализовал C / C++ lexer как подпрограмму (на самом деле это сложный класс), которая вызывается из грамматики парсер. Никаких проблем с этим осуществлением не возникло.

  3. "общение процессов". Мне кажется, это перебор. В этом подходе нет ничего плохого, но, может быть, лучше держать вещи простыми? Многоядерный аспект. Компиляция одного файла является относительно редким случаем. Я бы рекомендовал загрузить каждое ядро со своим файлом.

Я не вижу других разумных вариантов combiming лексер и парсер вместе.

Я написал эти заметки, думая о компиляции источников программного проекта. Разбор короткого запроса запроса-это совсем другое дело, и причины могут существенно отличаться. Мой ответ основан на моем собственном опыте. Другие люди могут видеть это по-разному.


отношение лексер-парсер проще, чем самый общий случай coroutines, потому что в целом связь односторонняя; парсер не должен отправлять информацию обратно в лексер. Вот почему метод eager generation работает (с некоторым штрафом, хотя это означает, что вы можете отказаться от ввода раньше).

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

хотя C++ не имеет язык поддержка coroutines, можно использовать библиотека поддержка, в частности волокнами. В Unix setcontext семья-один из вариантов; другой-использовать многопоточность, но с синхронно очередь (по существу однопоточная, но переключение между две нити управления).


также учтите для #1, что вам не нужны токены lex, например, если есть ошибка, и, кроме того, вы можете работать с низкой пропускной способностью памяти или ввода-вывода. Я считаю, что лучшим решением является использование парсеров, созданных такими инструментами, как Bison, где парсер вызывает лексер для получения следующего токена. Минимизирует требования к пространству и пропускной способности памяти.

#4 просто не стоит того. Лексика и синтаксический анализ по своей сути синхронны - есть только недостаточно обработки, чтобы оправдать расходы на связь. Кроме того, обычно вы анализируете/lex несколько файлов одновременно - это уже может максимизировать все ваши ядра сразу.


то, как я справляюсь с этим в моем проекте toy buildsystem, - это класс "File reader" с функцией bool next_token(std::string&,const std::set<char>&). Этот класс содержит одну строку ввода (для целей отчетов об ошибках с номером строки). Функция принимает std::string ссылка для ввода маркера и std::set<char> который содержит символы" токен-окончание". Мой входной класс-это парсер и лексер, но вы можете легко разделить его, если вам нужно больше фантазии. Поэтому функции синтаксического анализа просто вызывают next_token и может сделайте свое дело, включая очень подробный вывод ошибок.

Если вам нужно сохранить дословный ввод, вам нужно будет сохранить каждую строку, прочитанную в vector<string> или что-то, но не хранить каждый токен отдельно и/или двойной y.

код, о котором я говорю, находится здесь:

https://github.com/rubenvb/Ambrosia/blob/master/libAmbrosia/Source/nectar_loader.cpp

(ищите ::next_token и extract_nectar функция где все начинается)