В настоящее время я занимаюсь созданием PHP Parser, написанным на PHP, поскольку в моем предыдущем вопросе не появилось никакого синтаксического анализатора. Сам анализатор работает достаточно хорошо.
Теперь, очевидно, парсер сам по себе мало пользы (помимо статического анализа). Я хотел бы применить преобразования к AST, а затем скомпилировать его обратно в исходный код. Применение преобразований не является проблемой, обычно должен выглядеть обычный шаблон посетителя.
В настоящее время моя проблема заключается в том, как скомпилировать АСТ обратно в исходный код. В основном я вижу две возможности:
На данный момент я хотел бы сосредоточиться на 1., поскольку 2. кажется довольно трудно выполнить (но если у вас есть советы по этому поводу, я бы хотел их услышать).
Но я не уверен, какой шаблон дизайна можно использовать для компиляции кода. Самый простой способ реализовать это – добавить метод ->compile
ко всем узлам. Недостаток, который я вижу здесь, заключается в том, что было бы довольно сложно изменить форматирование сгенерированного вывода. Для этого нужно будет изменить сами узлы. Таким образом, я ищу другое решение.
Я слышал, что шаблон Visitor можно использовать и для этого, но я не могу представить, как это должно работать. Поскольку я понимаю шаблон посетителя, у вас есть некоторый NodeTraverser
который NodeTraverser
рекурсивно по всем узлам и вызывает метод ->visit
для Visitor
. Это звучит довольно многообещающе для манипуляции с узлами, где метод Visitor->visit
может просто изменить узел, который был передан, но я не знаю, как его можно использовать для компиляции. Очевидной идеей было бы итерации дерева узлов от листьев до корня и замены посещенных узлов исходным кодом. Но это как-то не кажется очень чистым решением?
Проблема преобразования АСТ в исходный код обычно называется «красивой печатью». Есть два тонких варианта: регенерация текста, соответствующего оригиналу, насколько это возможно (я называю эту «печать верности») и красивую печать, которая генерирует красиво отформатированный текст. И как вы печатаете вопросы в зависимости от того, будут ли кодеры работать над регенерированным кодом (они часто хотят печатать верность), или ваше единственное намерение состоит в том, чтобы скомпилировать его (в этот момент любая законная симпатичная печать в порядке).
Для корректной печати довольно обычно требуется больше информации, чем собирает классический парсер, что усугубляется тем фактом, что большинство генераторов парсеров не поддерживают эту сборку дополнительных данных. Я называю парсеров, которые собирают достаточно информации, чтобы сделать это хорошо «реорганизаторы парсеров». Подробнее см. Ниже.
Основополагающим способом симплинтинга является прохождение AST («Шаблон посетителя», как вы выразились) и генерирование текста на основе содержимого узла AST. Основной трюк: вызвать дочерние узлы слева направо (при условии, что это порядок исходного текста), чтобы генерировать текст, который они представляют, вставляя дополнительный текст, подходящий для этого типа узла AST. Чтобы отпечатать блок утверждений, у вас может быть следующий psuedocode:
PrettyPrintBlock: Print("{"}; PrintNewline(); Call PrettyPrint(Node.children[1]); // prints out statements in block Print("}"); PrintNewline(); return; PrettyPrintStatements: do i=1,number_of_children Call PrettyPrint(Node.children[i]); Print(";"); PrintNewline(); // print one statement endo return;
Обратите внимание, что это выплескивает текст «на лету», когда вы посещаете дерево.
Есть ряд подробностей, которые вам нужно решить:
Для узлов AST, представляющих литералы, вам необходимо восстановить буквальное значение. Это сложнее, чем кажется, если вы хотите, чтобы ответ был точным. Печать чисел с плавающей точкой без потери точности намного сложнее, чем кажется (ученые ненавидят ее, когда вы повредите значение Pi). Для строковых литералов вам необходимо восстановить кавычки и содержимое литерала строки; вы должны быть осторожны, чтобы регенерировать escape-последовательности для символов, которые должны быть экранированы. Строковые литералы с двумя строками PHP могут быть немного сложнее, поскольку они не представлены одиночными токенами в AST. (Наш PHP Front End (реинжиниринговый синтаксический анализатор / prettyprinter представляет их по существу как выражение, которое объединяет фрагменты строки, позволяя применять преобразования в строке «литерал»).
Интервал: некоторые языки требуют пробелов в критических местах. Токены ABC17 42 лучше не печататься как ABC1742, но для маркеров (ABC17) их можно напечатать как (ABC17). Один из способов решения этой проблемы – разместить пространство везде, где оно законно, но людям не понравится результат: слишком много пробелов. Не проблема, если вы только компилируете результат.
Новые строки: языки, которые допускают произвольные пробелы, могут быть технически регенерированы как одна строка текста. Люди ненавидят это, даже если вы собираетесь скомпилировать результат; иногда вам приходится смотреть на сгенерированный код, и это делает невозможным. Таким образом, вам нужен способ введения новых строк для узлов AST, представляющих основные элементы языка (операторы, блоки, методы, классы и т. Д.). Обычно это не сложно; при посещении узла, представляющего такую конструкцию, распечатайте конструкцию и добавьте новую строку.
Вы обнаружите, что если вы хотите, чтобы пользователи принимали ваш результат, вам нужно будет сохранить некоторые свойства исходного текста, который вы, как правило, не собираетесь хранить для литералов, вам, возможно, придется регенерировать основание литерала; кодеры, введя число как шестнадцатеричный литерал, не счастливы, когда вы регенерируете десятичный эквивалент, даже если это означает точно то же самое. Аналогично, строки должны иметь «оригинальные» кавычки; большинство языков допускают «или» как символы строковой кавычки, а люди хотят, чтобы они изначально использовались. Для PHP, в котировке которых вы используете вопросы, и определяет, какие символы в строковом литерале должны быть экранированы. Некоторые языки допускают ключевые слова верхнего или нижнего регистра ( или даже аббревиатуры), и имена переменных верхнего и нижнего регистра, означающие одну и ту же переменную, снова оригинальные авторы обычно хотят вернуть исходный корпус. PHP имеет забавные символы в разных типах идентификаторов (например, «$»), но вы обнаружите, что он не всегда существует (см. переменные $ в литеральных строках). Часто люди хотят, чтобы их оригинальное форматирование макета, для этого вам нужно хранить информацию о столбцах для конкретных токенов и иметь правила правильной печати о том, когда использовать этот столбец, числовые данные, чтобы позиционировать текст с отпечатками, где в том же столбце, когда это возможно, и что делать, если строка с таким удаленным цветом заполняется за этой колонкой.
Комментарии: Большинство стандартных парсеров (включая тот, который вы реализовали с помощью парсера Zend, я уверен) полностью отбрасывают комментарии. Опять же, люди ненавидят это и отклонят довольно отпечатанный ответ, в котором теряются комментарии. Это основная причина, по которой некоторые prettyprinters пытаются восстановить код с использованием исходного текста (другой – скопировать исходный макет кода для печати верности, если вы не указали информацию о столбцах). ИМХО, правильным трюком является захват комментариев в АСТ, так что трансформации АСТ могут также проверять / генерировать комментарии, но каждый делает свой собственный выбор дизайна.
Вся эта «дополнительная» информация собирается хорошим парсером для реинжиниринга. Обычные парсеры обычно не собирают ни одного из них, что затрудняет печатание приемлемых АСТ.
Более принципиальный подход отличает красивую печать, цель которой – хорошее форматирование, от печати верности, целью которой является регенерация текста в соответствии с исходным исходным кодом в максимальной степени. Должно быть ясно, что на уровне терминалов вы в значительной степени хотите печатать верность. В зависимости от вашей цели вы можете красиво печатать с хорошим форматированием или верностью печати. Стратегия, которую мы используем, заключается в том, чтобы по умолчанию печатать достоверность, когда AST не был изменен, и красивую печать там, где она есть (потому что часто механизм изменения не имеет никакой информации о номерах колонок или числовых радиусах и т. Д.). Преобразования отпечатывают узлы AST, которые вновь генерируются как «нет данных верности».
Организованный подход к красивой печати заключается в том, чтобы понять, что практически все текстовые языки программирования хорошо отображаются в терминах прямоугольных блоков текста. (Генератор документов Knuth's TeX также имеет эту идею). Если у вас есть набор текстовых полей, представляющих фрагменты регенерированного кода (например, примитивные поля, сгенерированные непосредственно для терминальных токенов), вы можете представить себе операторов для компоновки этих полей: Горизонтальная композиция (стекайте одно поле справа от другого) Вертикальные (складываются друг над другом, они заменяют новые строки), Отступ (Горизонтальная композиция с полем пробелов) и т. Д. Затем вы можете создать свой симпатичный принтер, построив и составив текстовые поля:
PrettyPrintBlock: Box1=PrimitiveBox("{"); Box2=PrimitiveBox("}"); ChildBox=PrettyPrint(Node.children[1]); // gets box for statements in block ResultBox=VerticalBox(Box1,Indent(3,ChildBox),Box2); return ResultBox; PrettyPrintStatements: ResultBox=EmptyBox(); do i=1,number_of_children ResultBox=VerticalBox(ResultBox,HorizontalBox(PrettyPrint(Node.children[i]); PrimitiveBox(";") endo return;
Реальное значение в этом случае – любой узел может составлять текстовые поля, созданные его дочерними элементами в произвольном порядке, с произвольным промежуточным текстом. Вы можете таким образом перестроить огромные блоки текста (представьте себе, что VBox использует методы класса в порядке имен меток). Никакой текст не выливается, как встречалось; только когда достигнут корень или какой-либо узел АСТ, где известно, что все дочерние ящики были созданы правильно.
Наш инструментарий DMS Software Reengineering Toolkit использует этот подход для отрисовки всех языков, которые он может анализировать (включая PHP, Java, C # и т. Д.). Вместо того, чтобы привязывать вычисления ящиков к узлам AST через посетителей, мы присоединяем вычисления ящиков в текстовой заметке, специфичной для домена
непосредственно к правилам грамматики, позволяя нам кратко выражать грамматику (парсер) и симпатичный («анти-парсер») в одном месте. Правила шаблона niceprinter автоматически компилируются DMS в посетителя. Симпатичный принтер должен быть достаточно умным, чтобы понять, как в нем играют комментарии, и это откровенно немного загадочно, но вам нужно только сделать это один раз. Пример DMS:
block = '{' statements '}' ; -- grammar rule to recognize block of statements <<PrettyPrinter>>: { V('{',I(statements),'}'); };
Вы можете увидеть больший пример того, как это делается для языка программирования Oberon Вирта PrettyPrinter, показывающего, как сочетаются правила грамматики и правила красивой печати. Внешний вид PHP выглядит так, но его намного больше, очевидно.
Более сложный способ сделать красивую печать состоит в том, чтобы создать транслятор, ориентированный на синтаксис (средство, пройти дерево и построить текст или другие структуры данных в порядке дерева) для создания текстовых полей в специальном текстовом поле AST. Текстовый блок AST затем красиво напечатан другим деревом, но действия для него в основном тривиальны: напечатайте текстовые поля. См. Этот технический документ: Допечатная подготовка для реинжиниринга программного обеспечения
Еще один момент: вы, конечно, можете сами построить все это оборудование. Но по той же причине, что вы решили использовать генератор парсера (его большая работа, чтобы сделать его, и эта работа не способствует вашей цели интересным способом) по той же причине вы хотите выбрать вне- полка симпатичный принтер. Существует множество генераторов парсеров. Не так много симпатичных генераторов. [DMS является одним из немногих, которые имеют встроенные функции.]