Как сделать компилятор на сайте
Для создания компилятора важно понимать следующее. Компилятор имеет несколько внутренних представлений.
Далее мы будем рассматривать только такие языки, в которых текст программы достаточно перевести в синтаксическое дерево, что означает, что семантика конструкций - такая же, как и в Паскале. Например, это означает, что целый тип неявно преобразуется в вещественный, но не наоборот.
Следует отметить, что в большинстве ситуаций за счёт проведения дополнительных проверок перевода в узлы синтаксического дерева вполне достаточно. Далее это станет понятнее на конкретных примерах.
Рассмотрим создание простого языка, который мы назовём Oberon00. Это - подмножество языка Oberon с двумя типами - INTEGER и BOOLEAN - и возможностью вызывать процедуры из внешних модулей.
Что нужно для создания компилятора
Кроме этого, обычно требуется системный модуль, содержащий стандартные подпрограммы. Его можно создать на языке Паскаль. Пусть для Оберона00 этот модуль называется Oberon00System.pas. Требуется откомпилировать его в .pcu и затем
Для того чтобы сразу увидеть конечный результат, можно скопировать полный комплект разработчика парсеров.
Что входит в комплект разработчика парсеров
Далее разберем последовательно, как создать парсер.
Лексический анализатор GPLex и синтаксический анализатор GPPG
Итак, наша задача - разобрать текст программы и по нему получить синтаксическое дерево программы. Такой построитель синтаксического дерева программы будем называть парсером или синтаксическим анализатором. Для работы синтаксического анализатора необходим также сканер или лексический анализатор, который разбивает программу на лексемы - неделимые слова. Например, в языке Паскаль лексемами являются ключевые слова, идентификаторы, числовые и строковые константы, знаки операций (:= <> и т.д.).
Синтаксические анализаторы обычно создаются с помощью специальных программ, называемых генераторами компиляторов. Одним из наиболее известных генераторов компиляторов является Yacc (Yet Another Compiler of Compilers) - и в паре с ним работает генератор лексических анализаторов Lex.
Создание лексического анализатора
Класс Scanner
Создаваемый в результате компиляции .lex-файла класс Scanner содержит несколько важных методов и свойств. Рассмотрим их подробнее.
- Функция int yylex() возвращает уникальный номер лексемы. Все лексемы хранятся в перечислимом типе Tokens. По-существу, возвращается номер константы в перечислимом типе Tokens - например, для лексемы ID возвращается (int)Tokens.ID
- Помимо уникального номера некоторые лексемы должны возвращать дополнительные значения. Таким значением для идентификатора является его строковое представление, для INTNUM - целое число и т.д. Дополнительное значение для некоторых лексем возвращается в свойстве yylval, которое имеет тип ValueType и содержит ряд полей различных типов - для каждой лексемы может быть предусмотрено своё поле. Например, для лексемы ID предусмотрено поле string sVal, а для лексемы INTNUM - поле iVal.
Объяснение того, как это поле сопоставляется с типом лексемы, отложим до знакомства с содержимым файла .y
- Свойство yytext возвращает строковое представление текущей лексемы
- Свойство yylloc содержит положение лексемы в тексте, задаваемое типом LexLocation из пространства имен QUT.Gppg. Он хранит координаты начала и конца лексемы
Файл .lex
Лексический анализатор обычно создается в файле с расширением .lex. Он имеет вид:
Рассмотрим файл лексического анализатора Oberon00.lex:
После обработки генератором лексических анализаторов GPLex мы получим одноименный cs-файл Oberon00.cs, содержащий класс Scanner лексического анализатора.
Разберем содержимое .lex-файла подробнее.
Секция определений
Вначале рассмотрим первую часть, содержащую определения лексем:
Строка %namespace GPPGParserScanner означает, что класс сканера будет помещен в пространство имен GPPGParserScanner
Строка %using PascalABCCompiler.Oberon00Parser; приводит к генерации соответствующей строки в cs-файле. Пространство имен PascalABCCompiler.Oberon00Parser полностью находится во вспомогательном файле Oberon00ParserTools.cs, который содержит различные глобальные описания и подпрограммы (подробнее содержимое этого файла будет объяснено позже).
Далее идёт описание некоторых лексем в виде регулярных выражений. Например, целое число INTNUM представляет собой последовательность одной или более цифр +, а идентификатор ID - последовательность букв или цифр, начинающуюся с буквы: *
Секция правил
Вторая секция (между первым и вторым %%) является основной и содержит действия, которые необходимо выполнить, когда распознана та или иная лексема.
Здесь приведены действия в случае если встречена та или иная лексема. Большинство действий просто возвращает порядковый номер лексемы в перечислимом типе Tokens.
Последняя лексема - INVISIBLE - символ с кодом 1 - необязательна и является вспомогательной для компилятора выражений языка (необходим для поддержки IntelliSense).
В случае лексемы INTNUM в поле yylval.iVal дополнительно возвращается целое число, соответствующее лексеме: int.Parse(yytext). Обратим внимание, что здесь нет никакой защиты от неправильного преобразования (например, в случае очень длинного целого). Причина - чтобы пока не усложнять изложение обработкой ошибок.
В случае лексемы ID вначале проверяется, не является ли она ключевым словом, и если является, то возвращается целое, соответствующее лексеме ключевого слова (например, для BEGIN возвращается (int)Tokens.BEGIN), а если не является, то возвращается (int)Tokens.ID и параллельно в поле yylval.sVal возвращается строковое представление идентификатора. Именно на этом уровне мы можем задать, чувствителен ли наш язык к регистру (например, преобразованием всех идентификаторов к UpperCase).
Секция правил завершается следующим кодом:
Этот код позволяет для каждой лексемы получать её местоположение в тексте. Вникать в этот код не надо - он просто необходим.
Секция пользовательского кода
Наконец, в секции пользовательского кода содержится единственный переопределенный в классе Scanner метод
Генерация кода лексического анализатора
Для генерации кода лексического анализатора следует выполнить команду:
Здесь gplex.exe - генератор лексических анализаторов, хранящийся в папке GPLex_GPPG, параметр /unicode указывает на то, что созданный лексический анализатор будет распознавать файлы в кодировке unicode (заметим, что он будет распознавать и однобайтные кодировки).
Создание синтаксического анализатора
Общая информация
Синтаксический анализатор - ядро нашего компилятора. Он строится по .y - файлу, в котором записываются правила грамматики языка и действия, которые выполняются по этим правилам. Каждое из действий создает соответствующий узел в синтаксическом дереве. Все узлы синтаксического дерева представляют собой иерархию классов с общим предком syntax_tree_node и описаны во внешней библиотеке SyntaxTree.dll.
Для понимания дальнейшего следует иметь некоторое начальное представление о грамматиках языков программирования, о том, как они порождают языки и об автоматах, которые проверяют, принадлежит ли данная цепочка символов языку, порождаемому грамматикой. Более конкретно, следует представлять, что такое контекстно-свободная грамматика, что Yacc-Lex системы работают с так называемыми LL(k) грамматиками, являющимися достаточно широким подмножеством контекстно-свободных грамматик.
В процессе построения грамматик придется сталкиваться с недостижимыми и циклическими продукциями грамматик, неоднозначными грамматиками. Необходимо уметь исправлять подобные ситуации хотя бы методом проб и ошибок. Особенно необходимо понимать, что Shift-Reduce - конфликты грамматик допустимы и разрешаются в пользу более длинной правой части продукции, а Reduce-Reduce - конфликты недопустимы и свидетельствуют об ошибках проектирования грамматики.
Соответствующую информацию можно прочитать в книге Ахо "Компиляторы: принципы, технологии, инструменты".
Некоторые классы синтаксического дерева
Как уже было сказано, синтаксическое дерево состоит из объектов классов - наследников syntax_tree_node. Перечислим некоторые важные классы - наследники syntax_tree_node, которые будут нами использованы для реализации парсера Оберона00:
Каждый класс имеет свои поля, все необходимые поля заполняются обычно в конструкторе класса. В базовом классе syntax_tree_node имеется поле source_context типа SourceContext, определяющее местоположение конструкции, соответствующей синтаксическому узлу, в тексте программы. Именно поэтому каждый конструктор синтаксического дерева содержит последним параметром объект класса SourceContext.
Просмотреть все узлы синтаксического дерева, их свойства и методы можно с помощью утилиты Просмотрщик классов синтаксического дерева
Правила грамматики
Правила грамматики содержат терминальные символы (терминалы), которые распознаются лексическим анализатором, и нетерминальные символы (нетерминалы). Каждое правило грамматики выражает нетерминал через другие терминалы и нетерминалы. Например, для оператора присваивания имеется следующее правило:
Здесь нетерминал AssignOperator выражается через терминалы ID и ASSIGN и нетерминал expr, который будет определен в другом правиле.
Нетерминал, с которого начинается разбор, называется стартовым символом грамматики. Этот нетерминал описывает всю программу. В нашей грамматике это module, а соответствующее стартовое правило имеет вид:
В конечном итоге все нетерминалы выражаются через терминалы, образующие основную программу. Соответствующий процесс получения программы из стартового символа называется выводом, а правила грамматики - правилами вывода.
В каждом правиле может быть несколько альтернатив, разделяемых символом |:
Правила могут быть рекурсивными. Например, правило для фактических параметров имеет вид:
Обратим внимание, что данное правило является леворекурсивным, поскольку рекурсивное вхождение нетерминала factparams в правую часть правила находится с левой стороны. Для рассматриваемых нами генераторов компиляторов леворекурсивные правила являются предпочтительнее праворекурсивных.
Формат .y-файла
.y-файл имеет такой же формат, как и .lex:
Раздел определений .y-файла
Общий вид
Рассмотрим раздел определений файла Oberon00.y
Начальные строки
Здесь root - корневой узел синтаксического дерева программы, он явно описан как поле класса GPPGParser. Описываемый во второй строке конструктор носит технический характер - его просто необходимо в этом месте написать. Думаю, что эта строчка связана с неудачным проектированием генератора парсеров GPPG - разработчик забыл в генерируемый класс включить этот нужный конструктор.
означает, что компиляция .y-файла с помощью gppg.exe будет осуществляться в файл oberon00yacc.cs
говорит о том, что класс парсера будет иметь имя GPPGParser
означают подключение в коде парсера соответствующих пространств имен (точки с запятой в конце строк не нужны в отличие от лексического анализатора - видимо, это ошибка проектирования gppg), а строка
означает, что класс парсера будет помещен в пространство имен GPPGParserScanner.
означает, что стартовым в нашей грамматике является нетерминал module (обычно эту строку можно не писать - считается, что стартовым является нетерминал в левой части первого правила).
Описание используемых терминалов
Все используемые терминалы должны быть описаны в секции определений следующим образом:
Именно по этим определениям формируется перечислимый тип Tokens, который встречался в .lex-файле.
Кроме этого, некоторым терминалам и нетерминалам можно задавать тип.
Типы терминалов и нетерминалов
Большинство нетерминалов и некоторые терминалы должны иметь тип. Например, терминал ID имеет тип string, а терминал INTNUM - тип int. Для задания этих типов используют структуру-объединение вида
Если с терминалом или нетерминалом необходимо связать значение некоторого типа, то поле этого типа описывается в структуре union. Например, чтобы связать с терминалом ID значение строкового типа, необходимо описать в структуре union поле
После этого необходимо описать типизированный терминал следующим образом:
Аналогично чтобы связать с нетерминалом Assignment тип statement, необходимо описать в структуре union поле
и после этого связать поле st с нетерминалом Assignment следующим образом:
Полный код описаний для типов терминалов и нетерминалов, а также полей структуры union имеет вид:
Приоритеты операций языка
Чтобы не задавать приоритеты операций в грамматике, в Yacc-системах используется секция задания приоритетов операций. Она имеет вид:
Операции задаются в порядке от самого низкого приоритета до самого высокого.
После такого задания приоритетов операций в грамматике выражений можно писать:
и это не вызовет неоднозначности грамматики.
Секция правил грамматики
Попробуем проследить, как распознаются правила грамматики.
Правило Assignment
Рассмотрим вначале правило для оператора присваивания:
Здесь формируется узел синтаксического дерева для оператора присваивания. assign - это класс - наследник syntax_tree_node, который хранит всю синтаксическую информацию об операторе присваивания: идентификатор в левой части, выражение в правой части, тип оператора присваивания (в данном случае обычное присваивание, есть ещё += *= и т.д.), а также позиция конструкции присваивания в тексте программы. В данной записи $$ обозначает нетерминал в левой части, $1 - первый символ (нетерминал или терминал) в правой части правила, $2 - второй символ в правой части правила и т.д. Таким образом, $$ соответствует символу Assignment, $1 - символу ident, а $3 - символу expr. Очень важно, что если типы соответствующих символов были прописаны в секции описаний, то выражения $$, $1, $2 и т.д. имеют ровно эти типы. Узнаем в разделе описаний типы Assignment, ident и expr:
Теперь заглянём в структуру union:
Таким образом, можно сделать вывод, что $$ имеет тип statement, $1 - тип ident, а $3 - тип expression. Если окажется, что для какого-то нетерминала или терминала не определен тип, то считается, что соответствующий символ $. имеет тип Object.
Как правило, в большинстве узлов @$ будет передаваться в качестве последнего параметра конструктора.
Правило StatementSequence
Правило для StatementSequence имеет вид:
Здесь формируется список операторов, которые в программе разделены точкой с запятой. Обратим внимание, что за счёт леворекурсивности этого правила первым заканчивается разбор первого оператора Statement - в этот момент мы и создаём statement_list вызовом конструктора. В рекурсивной части считается, что statement_list уже создан, и мы добавляем в него следующий Statement с помощью метода Add класса statement_list.
Во втором правиле есть и ещё одна тонкость, рассмотрим её подробнее - она часто встречается. Рассмотрим ещё раз второе правило внимательнее:
- всё равно после присваивания $$ = $1 переменные $$ и $1 указывают на один объект.
Правила Statement
Проследим далее за правилами Statement:
Здесь нет действий в <>, поэтому по умолчанию всегда подразумевается действие $$ := $1, что нам и надо.
Рассмотрим ещё несколько правил, в которых есть ранее не встречавшиеся моменты.
Правило ident
Чтобы не преобразовывать всякий раз строковый ID в узел синтаксического дерева ident, введено правило:
Правило для унарного минуса
Правило для унарного минуса имеет вид:
Ключевое слово %prec меняет в рамках одного правила приоритет операции MINUS и делает его таким же, как и у UMINUS. Заметим, что терминал UMINUS - фиктивный - он не может возникнуть при лексическом разборе, и задаётся только в секции приоритетов операций - последним и, значит, самым приоритетным.
Правило для всей программы
Вся программа на Oberon00 представляет собой модуль:
По этому правилу производятся следующие действия:
Поясним их подробнее. Во-первых, проверяется, совпадает ли имя модуля в начале и в конце программы и если нет. то генерируется семантическая ошибка с помощью PT.AddError (эта ошибка будет добавлена в окно ошибок компиляции). Заметим, что на этапе построения синтаксического дерева можно распознать и диагностировать некоторые синтаксические ошибки.
Затем в начало списка подключаемых модулей добавляется так называемый системный модуль Oberon00System - он написан на Паскале и должен быть откомпилирован в .pcu.
После этого формируется синтаксический узел для всей программы (класс program_module) и присваивается переменной root.
Создание системного модуля
Напишем для компилятора Oberon00 простой системный модуль на паскале Obеron00System.pas:
Именно он подключается автоматически к любой программе на Обероне в действиях для правила module:
Хотела установить на свой сайт, сделанный на движке wordpress встроенный компилятор, но не знаю как его установить на сайт.
Наверняка никак.
Вы не знали, что все готовые CMS и фреймворки - УГ?
Компиляция - это функционал сервера. Нет доступа к серверу, нет и компиляции.
На чистом PHP+JS+HTML+CSS пишите сайт.
> Вы не знали, что все готовые CMS и фреймворки - УГ?
Ну в руках умелого и умного человека -- обычные инструменты для создания сайтов. В руках пейсателей на 10 языках программирования -- УГ, они же с ними не работали, вот и обсирают подряд все, в чем они не разбираются и чего не понимают.
> На чистом PHP+JS+HTML+CSS пишите сайт.
СУБД не забыли добавить?
Онлайн-компиляторы созданы для того, чтобы упростить проверку кода. Вам не нужно скачивать и устанавливать разные приложения на ноутбук: чтобы использовать компилятор, нужно только подключение к интернету. Вы можете проверить код на работоспособность, увидеть ошибки и результат выполнения программы.
Разберемся, какие виды компиляторов существуют и как с ними работать.
Мультиязычные онлайн-компиляторы
-
— это компилятор, в котором можно мгновенно запустить свой код. Он работает с C ++, Java, Python, Perl, Scala и многими другими языками. Этот компилятор можно использовать даже с мобильных устройств, а также генерировать URL-адрес кода и делиться им с коллегами. А чтобы ускорить процесс работы с кодом, IDE предлагает использовать горячие клавиши.
Мультиязычный онлайн-компилятор IDE GeeksforGeeks
-
— это бесплатный онлайн-компилятор, где можно скомпилировать и запустить код за несколько секунд. Он поддерживает более 60 языков программирования. Интересная функция Ideone: в компиляторе вы можете сделать свой код общедоступным, частным или секретным и обмениваться им с участниками вашей команды. В Ideone есть примеры рабочих кодов на разных языках, которые можно использовать.
Бесплатный мультиязычный онлайн-компилятор Ideone
- JDOODLE — это компилятор и редактор для хранения, запуска и обмена кодом в любое время и в любом месте. В JDOODLE можно выбрать язык программирования из 72 вариантов (сюда входят MySQL и MongoDB). Компилятор легко встроить в блог или веб-сайт, если нужно продемонстрировать работу кода в интерактивной среде. Ни одна строчка не потеряется — все выполненные блоки кода можно найти в компиляторе.
Мультиязычный онлайн-компилятор JDOODLE
Онлайн-компиляторы для Python
-
— один из распространенных и самых быстрых онлайн-компиляторов Python для запуска исходного кода. Поддерживает версии Python 2.0 и 3.0, помогает выявить ошибки. Также в Tutorialspoint можно скачать свой проект или поделиться им (но для этого нужно зарегистрироваться на сайте). Еще одно достоинство — интерфейс компилятора можно настроить под себя и увеличить шрифт, чтобы не напрягать глаза и не разглядывать крохотные буквы.
Пример кода в Tutorialspoint
-
— компилятор поддерживающий Python и другие языки программирования. Он предлагает как бесплатную, так и платную версии. В платном варианте вы найдете полезные функции для разработки проектов корпоративного уровня, но для простой проверки кода подойдут и базовые функции компилятора. Интерфейс Paiza доступен на трех языках: испанском, японском и английском. В Paiza вы можете привязать учетную запись git и включить автосинхронизацию кода, сохранить код и редактировать его в любое удобное время, а также настроить интерфейс компилятора под себя.
Пример кода в Paiza.io
-
— компилятор с минималистичным дизайном. Он поддерживает версию Python 3.8. Особенность этого компилятора в том, что он поддерживает интерактивное выполнение программы, которое позволяет вводить программные данные в режиме реального времени. В компиляторе также есть опция совместного использования кода: вы можете сохранить код в облаке и использовать его вместе с коллегами.
Пример кода в Online Python
Онлайн-компиляторы для JavaScript
-
предназначен для написания, тестирования и обмена фрагментами кода на JavaScript, HTML и CSS. Главное преимущество компилятора: он помещает весь этот код в одно окно и вам не приходится переключаться между вкладками браузера, чтобы увидеть результат работы. Компилятор поддерживает Angular, React, Vue, jQuery и многие другие библиотеки, а также CSS, SCSS, SASS, PostCSS, Normalized CSS.
Пример кода в JSFiddle
-
— компилятор, который очень похож на JSFiddle. Здесь также есть несколько панелей для отображения строк кода HTML, CSS и JavaScript, а также результата их выполнения. Все изменения в коде пользователь видит в режиме реального времени. Программа поддерживает библиотеки React, Vue, Bulma, Bootstrap, jQuery и не только.
Пример кода в PLAYCODE
Онлайн-компиляторы для PHP
-
— это простой и практичный онлайн-редактор, подойдет для начинающих PHP-разработчиков. Одно из главных достоинств компилятора — понятный интерфейс.
Пример кода в WritePHPOnline
Пример кода в PHP Sandbox
Онлайн-компиляторы для Java
-
— удобный редактор кода. Codiva поддерживает не только Java 9, но C, C ++ и Python. Основная особенность Codiva в том, что программа компилирует код по мере ввода, анализирует ошибки и показывает их в редакторе. В нем есть автозаполнение — это позволит сэкономить время. Но чтобы пользоваться другими возможностями компилятора (например, сохранять код), необходимо будет зарегистрироваться на сайте.
Пример кода в Codiva.io
Пример кода в OnlineGDB
Главная > Инструменты > Выполнить код в браузере: 12 онлайн-компиляторов, которые упростят вашу жизнь
Highload нужны авторы технических текстов. Вы наш человек, если разбираетесь в разработке, знаете языки программирования и умеете просто писать о сложном!
Откликнуться на вакансию можно здесь .
На протяжении последних шести месяцев я работал над созданием языка программирования (ЯП) под названием Pinecone. Я не рискну назвать его законченным, но использовать его уже можно — он содержит для этого достаточно элементов, таких как переменные, функции и пользовательские структуры данных. Если хотите ознакомиться с ним перед прочтением, предлагаю посетить официальную страницу и репозиторий на GitHub.
Введение
Я не эксперт. Когда я начал работу над этим проектом, я понятия не имел, что делаю, и всё еще не имею. Я никогда целенаправленно не изучал принципы создания языка — только прочитал некоторые материалы в Сети и даже в них не нашёл для себя почти ничего полезного.
Тем не менее, я написал абсолютно новый язык. И он работает. Наверное, я что-то делаю правильно.
В этой статье я постараюсь показать, каким образом Pinecone (и другие языки программирования) превращают исходный код в то, что многие считают магией. Также я уделю внимание ситуациям, в которых мне приходилось искать компромиссы, и поясню, почему я принял те решения, которые принял.
Текст точно не претендует на звание полноценного руководства по созданию языка программирования, но для любознательных будет хорошей отправной точкой.
Первые шаги
Компилируемый или интерпретируемый?
Компилятор анализирует программу целиком, превращает её в машинный код и сохраняет для последующего выполнения. Интерпретатор же разбирает и выполняет программу построчно в режиме реального времени.
Технически любой язык можно как компилировать, так и интерпретировать. Но для каждого языка один из методов подходит больше, чем другой, и выбор парадигмы на ранних этапах определяет дальнейшее проектирование. В общем смысле интерпретация отличается гибкостью, а компиляция обеспечивает высокую производительность, но это лишь верхушка крайне сложной темы.
Я хотел создать простой и при этом производительный язык, каких немного, поэтому с самого начала решил сделать Pinecone компилируемым. Тем не менее, интерпретатор у Pinecone тоже есть — первое время запуск был возможен только с его помощью, позже объясню, почему.
Прим. перев. Кстати, у нас есть краткий обзор серии статей по созданию собственного интерпретатора — это отличное упражнение для тех, кто изучает Python.
Выбор языка
Своеобразный мета-шаг: язык программирования сам является программой, которую надо написать на каком-то языке. Я выбрал C++ из-за производительности, большого набора функциональных возможностей, и просто потому что он мне нравится.
Но в целом совет можно дать такой:
- интерпретируемый ЯП крайне рекомендуетсяписать на компилируемом ЯП (C, C++, Swift). Иначе потери производительности будут расти как снежный ком, пока мета-интерпретатор интерпретирует ваш интерпретатор;
- компилируемый ЯП можно писать на интерпретируемом ЯП (Python, JS). Возрастёт время компиляции, но не время выполнения программы.
Проектирование архитектуры
У структуры языка программирования есть несколько ступеней от исходного кода до исполняемого файла, на каждой из которых определенным образом происходит форматирование данных, а также функции для перехода между этими ступенями. Поговорим об этом подробнее.
Лексический анализатор / лексер
Строка исходного кода проходит через лексер и превращается в список токенов.
Первый шаг в большинстве ЯП — это лексический анализ. Говоря по-простому, он представляет собой разбиение текста на токены, то есть единицы языка: переменные, названия функций (идентификаторы), операторы, числа. Таким образом, подав лексеру на вход строку с исходным кодом, мы получим на выходе список всех токенов, которые в ней содержатся.
Обращения к исходному коду уже не будет происходить на следующих этапах, поэтому лексер должен выдать всю необходимую для них информацию.
При создании языка первым делом я написал лексер. Позже я изучил инструменты, которые могли бы сделать лексический анализ проще и уменьшить количество возникающих багов.
Одним из основных таких инструментов является Flex — генератор лексических анализаторов. Он принимает на вход файл с описанием грамматики языка, а потом создаёт программу на C, которая в свою очередь анализирует строку и выдаёт нужный результат.
Моё решение
Я решил оставить написанный мной анализатор. Особых преимуществ у Flex я в итоге не увидел, а его использование только создало бы дополнительные зависимости, усложняющие процесс сборки. К тому же, мой выбор обеспечивает больше гибкости — например, можно добавить к языку оператор без необходимости редактировать несколько файлов.
Синтаксический анализатор / парсер
Список токенов проходит через парсер и превращается в дерево.
Следующая стадия — парсер. Он преобразует исходный текст, то есть список токенов (с учётом скобок и порядка операций), в абстрактное синтаксическое дерево, которое позволяет структурно представить правила создаваемого языка. Сам по себе процесс можно назвать простым, но с увеличением количества языковых конструкций он сильно усложняется.
Bison
На этом шаге я также думал использовать стороннюю библиотеку, рассматривая Bison для генерации синтаксического анализатора. Он во многом похож на Flex — пользовательский файл с синтаксическими правилами структурируется с помощью программы на языке C. Но я снова отказался от средств автоматизации.
Преимущества кастомных программ
С лексером моё решение писать и использовать свой код (длиной около 200 строк) было довольно очевидным: я люблю задачки, а эта к тому же относительно тривиальная. С парсером другая история: сейчас длина кода для него — 750 строк, и это уже третья попытка (первые две были просто ужасны).
Тем не менее, я решил делать парсер сам. Вот основные причины:
В целесообразности решения меня убедило высказывание Уолтера Брайта (создателя языка D) в одной из его статей:
Абстрактный семантический граф
Переход от синтаксического дерева к семантическому графу
АСГ vs АСД
Грубо говоря, семантический граф — это синтаксическое дерево с контекстом. То есть, он содержит информацию наподобие какой тип возвращает функция или в каких местах используется одна и та же переменная. Из-за того, что графу нужно распознать и запомнить весь этот контекст, коду, который его генерирует, необходима поддержка в виде множества различных поясняющих таблиц.
Запуск
После того, как граф составлен, запуск программы становится довольно простой задачей. Каждый узел содержит реализацию функции, которая получает некоторые данные на вход, делает то, что запрограммировано (включая возможный вызов вспомогательных функций), и возвращает результат. Это — интерпретатор в действии.
Варианты компиляции
Вы, наверное, спросите, откуда взялся интерпретатор, если я изначально определил Pinecone как компилируемый язык. Дело в том, что компиляция гораздо сложнее, чем интерпретация — я уже упоминал ранее, что столкнулся с некоторыми проблемами на этом шаге.
Написать свой компилятор
Сначала мне понравилась эта мысль — я люблю делать вещи сам, к тому же давно хотел изучить язык ассемблера. Вот только создать с нуля кроссплатформенный компилятор — сложнее, чем написать машинный код для каждого элемента языка. Я счёл эту идею абсолютно не практичной и не стоящей затраченных ресурсов.
LLVM — это коллекция инструментов для компиляции, которой пользуются, например, разработчики Swift, Rust и Clang. Я решил остановиться на этом варианте, но опять не рассчитал сложности задачи, которую перед собой поставил. Для меня проблемой оказалось не освоение ассемблера, а работа с огромной многосоставной библиотекой.
Транспайлинг
Дальнейшие планы
Сейчас мне не достаёт необходимой практики, но в будущем я собираюсь от начала и до конца реализовать компилятор Pinecone с помощью LLVM — инструмент мне нравится и руководства к нему хорошие. Пока что интерпретатора хватает для примитивных программ, а транспайлер справляется с более сложными.
Заключение
Надеюсь, эта статья окажется кому-нибудь полезной. Я крайне рекомендую хотя бы попробовать написать свой язык, несмотря на то, что придётся разбираться во множестве деталей реализации — это обучающий, развивающий и просто интересный эксперимент.
Вот общие советы от меня (разумеется, довольно субъективные):
- если у вас нет предпочтений и вы сомневаетесь, компилируемый или интерпретируемый писать язык, выбирайте второе. Интерпретируемые языки обычно проще проектировать, собирать и учить;
- с лексерами и парсерами делайте, что хотите. Использование средств автоматизации зависит от вашего желания, опыта и конкретной ситуации;
- если вы не готовы / не хотите тратить время и силы (много времени и сил) на придумывание собственной стратегии разработки ЯП, следуйте цепочке действий, описанной в этой статье. Я вложил в неё много усилий и она работает;
- опять же, если не хватает времени / мотивации / опыта / желания или ещё чего-нибудь для написания классического ЯП, попробуйте написать эзотерический, типа Brainfuck. (Советуем помнить, что если язык написан развлечения ради, это не значит, что писать его — тоже сплошное развлечение. — прим. перев.)
Я делал довольно много ошибок по ходу разработки, но большую часть кода, на которую они могли повлиять, я уже переписал. Язык сейчас неплохо функционирует и будет развиваться (на момент написания статьи его можно было собрать на Linux и с переменным успехом на macOS, но не на Windows).
О том, что ввязался в историю с созданием Pinecone, ни в коем случае не жалею — это отличный эксперимент, и он только начался.
Читайте также: