Практика использования парсер-комбинаторов peco и оператора match для создания простых DSL на языке Python |
||
МЕНЮ Главная страница Поиск Регистрация на сайте Помощь проекту Архив новостей ТЕМЫ Новости ИИ Голосовой помощник Разработка ИИГородские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Искусственный интеллект Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Нейронные сети начинающим Психология ИИ Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Творчество ИИ Техническое зрение Чат-боты Авторизация |
2024-12-17 11:47 Задачи разработки компиляторов и интерпретаторов конфигурационных языков или даже полноценных Тьюринг-полных языков программирования время от времени встают перед разработчиками программного обеспечения. На практике, как правило, речь идёт о разработке предметно-ориентированных языков (англ. Domain Specific Language, DSL), проектируемых специально для решения узкого класса прикладных задач. DSL применяются, в том числе, для:
Классическая архитектура компилятора включает фронтенд (англ. Compiler Frontend) и бэкенд (англ. Compiler Backend). Фронтенд компилятора преобразует программу на языке высокого уровня в промежуточное представление — дерево абстрактного синтаксиса (англ. Abstract Syntax Tree, AST), выполняя также семантический анализ AST, а бэкенд синтезирует целевой код из AST, при необходимости применяя оптимизирующие преобразования. ЗамечаниеВ современных промышленных компиляторах, таких как LLVM и GCC, также выделяют стадию Middle-End, на которой к программе применяются оптимизирующие преобразования, не зависящие ни от входного, ни от выходного языка. Для упрощения мы относим эту стадию к бэкенду рассматриваемого в статье миниатюрного транслятора DSL, хотя такие его части, как система переписывания AST и графовое промежуточное представление, о которых пойдёт речь далее, можно было бы выделить в отдельную стадию Middle-End. Как указано в ГОСТ 19781-90, компиляция — это трансляция программы с языка высокого уровня в форму, близкую к программе на машинном языке. Трансляция — это преобразование программы, представленной на одном языке программирования, в программу на другом языке и в определенном смысле равносильную первой. На высоком уровне транслятор можно описать как: В настоящей статье рассматривается один из способов реализации DSL на примере разработки системы символьного дифференцирования, как в SymPy, с использованием парсер-комбинаторов peco и структурного сопоставления с образцом по PEP 636. Материал рассчитан на прикладных разработчиков, уже знакомых с Python, но, надеюсь, может быть полезен и продолжающим компиляторщикам. Средство для символьного дифференцирования выражений Рассмотрим следующую задачу. Пусть дан текст на формальном языке для описания простых арифметических выражений с поддержкой переменных и вызовов функций, причём грамматика этого языка в расширенной форме Бэкуса-Наура (англ. Extended Backus-Naur Form, EBNF) по стандарту ISO/IEC 14977:1996 имеет вид:
По грамматике выше, имена переменных и функций (см. правило Грамматика Примеры синтаксически корректных текстов на описанном языке включают следующие: Однако, синтаксическая корректность выражения ещё не гарантирует, что выражение вычисляет что-то полезное и осмысленное. Исключим из списка выше выражения, не выполняющие полезных арифметических операций, и получим следующий набор:
Именно такие выражения нас будут интересовать в следующих разделах. Фронтенд транслятора Классическая реализация фронтенда транслятора включает два этапа — лексический разбор и синтаксический разбор. На этапе лексического разбора строка с текстом программы преобразуется в список лексических единиц — токенов, а на этапе синтаксического разбора из списка токенов строится AST. Подобным образом работают такие инструменты, как flex и bison или python lex/yacc Д. Бизли. Однако, мы применим другой подход, известный в литературе как РВ-грамматика — грамматику, разбирающую выражение (англ. Parsing Expression Grammar, PEG). PEG, в отличие от контекстно-свободных грамматик, не могут быть неоднозначными — проверка альтернатив оператором Начало работы Для разбора нашего формального языка мы воспользуемся комбинаторами разбора, реализованными в библиотеке parsing expression combinators (peco). По сравнению с существующими аналогами [1, 2], библиотека Для начала работы необходимо подключить peco к нашему проекту. В случае, если мы работаем в git-репозитории, один из простейших способов сделать это — добавить peco в наш репозиторий как подмодуль git, выполнив из корня нашего репозитория команду: Теперь можно приступать к разработке фронтенда транслятора! Начнём с разбора имён переменных и функций: Отлично! Имена переменных по самому первому правилу в EBNF выше у нас получилось распознать. Функция Аналогичным образом разберём числа: Также разберём операторы, поддерживающиеся в нашем формальном языке: Для разбора имён, чисел и операторов нам оказалось достаточно регулярных выражений — и это ожидаемый результат, ведь простые последовательности букв или цифр — это регулярный язык, регулярный язык распознаётся конечным автоматом, а регулярное выражение позволяет задать такой конечный автомат. Реализуем теперь правило разбора для выражения Здесь регулярных выражений оказалось недостаточно, добавились новые комбинаторы!
Мы реализовали грамматику, успешно распознающую все выражения из вводной части! Но, всё-таки, ожидается, что результатом работы фронтенда транслятора окажется AST, построенное из текста программы. Внимательный читатель наверняка обратит внимание, что помимо полей
Перепишем нашу грамматику, используя стек для хранения и преобразования результатов распознавания: Отлично, мы получили AST, описанное при помощи вложенных друг в друга кортежей! Первый элемент кортежа — это оператор или имя функции, такую форму в дальнейшем будет удобно интерпретировать. Кстати, реализованную грамматику несложно доработать для поддержки пробелов и переносов строк. Читаемость грамматики можно улучшить, воспользовавшись комбинатором Грамматика заняла всего 20 строк кода! Бэкенд транслятора Бэкенд транслятора синтезирует целевой код из AST: Мы реализуем несколько модулей бэкенда транслятора для синтеза кода на разных языках. Синтез кода на DSL dot для Graphviz Начнём с визуализации вложенных друг в друга кортежей — структуры данных, которую в предыдущем разделе мы назвали AST. Нужно ведь показать, что у нас действительно получилось синтаксическое дерево! Сначала полезно преобразовать вложенные кортежи в графовое промежуточное представление (англ. Intermediate Representation, IR). Граф — это совокупность непустого множества вершин и множества связей между вершинами, но для упрощения работы мы сохраним вершины в словаре В операторе От графового IR легко перейти, например, к коду на DSL dot визуализатора graphviz: Визуализировать полученный код на языке dot легко в онлайн-версии graphviz: Синтез команд LaTeX Поскольку мы работаем с формулами и собираемся сделать систему символьного дифференцирования, было бы удобно смотреть на наши выражения не как на код или деревья, а как на настоящие формулы. Формулы мы сможем получить, если научимся транслировать наше AST в набор команд системы компьютерной вёрстки LaTeX. Для этого создадим словарь Визуализировать формулу LaTeX можно, например, в онлайн-редакторе LaTeX:
Система переписывания AST В настоящих компиляторах к AST, построенному фронтендом компилятора, применяется целый ряд оптимизирующих преобразований: выполняется свёртка констант (англ. constant folding), преобразование AST к IR в форме статического одиночного присваивания (англ. static single assignment form, SSA), которое затем переписывается для удаления недостижимого кода, экономии выражений, раскрутки циклов, ... Нашу систему символьного дифференцирования мы также реализуем как переписывающую систему (англ. term rewriting system, TRS) на основе таблицы производных функций. Алгоритм здесь следующий: если мы в процессе обхода AST встречаем поддерево, соответствующее формуле из левой части таблицы, то мы это поддерево заменяем на поддерево, соответствующее формуле из правой части таблицы: Для преобразования формулы в LaTeX мы воспользовались реализованной функцией Теперь обойдём AST, переписывая всё, что можно, при помощи функции Ура, посчитали! Но в конце первого слагаемого находится единица, которую обычно не записывают в ответе. Как нам избавиться от такой избыточности? Правильно, реализовать ещё один набор правил переписывания, для упрощения выражений! Отлично, убрали избыточность. А что, если после упрощения выражения при обходе AST сверху вниз появляется возможность упростить его ещё сильнее? Например, применение наших правил переписывания к следующему AST порождает выражение, всё ещё содержащее избыточное возведение в степень На самом деле, мы сейчас столкнулись с проблемой, часто возникающей в оптимизирующих компиляторах — после применения одних оптимизирующих преобразований может появиться возможность применить другие преобразования, итеративно «полируя» код в процессе компиляции. Простое решение нашей проблемы — найти неподвижную точку (англ. fixed point), то есть такое AST, для которого справедливо утверждение: Готово! Получаем желаемый результат:
Обзор основных результатов Кажется, нам удалось реализовать транслятор простого минималистичного DSL, который умеет дифференцировать и упрощать выражения, транслировать выражения в LaTeX-код для визуализации формул, а также в код на языке dot для визуализации деревьев при помощи graphviz. Архитектура нашего транслятора теперь имеет следующий вид: Нашим транслятором теперь можно пользоваться следующим образом:
Дальнейшее изучение Литература по компиляторам для новичков и продолжающих собрана в замечательном репозитории Что читать о разработке компиляторов? на GitHub. Кроме того, выделю материалы, которые заинтересуют и практикующих компиляторщиков, и им сочувствующих:
Отдельное спасибо Петру Советову @true-grue за ценные комментарии, позволившие (существенно!) упростить примеры кода в статье, а также за рецензирование материала в целом. Спасибо пользователям Habr за активное участие в обсуждении статьи, разъяснение материала по PEG в комментариях и за рекомендации, позволившие повысить качество рукописи. Источник: habr.com Комментарии: |
|