Это второй пост в моей серии Языки программирования Sea; см. Море №0. В этой статье я начну программировать Sea на основе грамматики, которую мы создали в прошлый раз. Я начну с реализации арифметических операций для Sea в интерпретаторе и транспилере. Это также заложит основу для Sea в целом.

Я буду писать код для Sea на Python. Как только Sea заработает, я перепишу код в самом Sea. Вы можете создать свой собственный проект Python или следовать моей отправной точке.

К сожалению, у меня не было времени, чтобы должным образом продолжить этот проект из-за школы. Я ожидаю, что развитие Sea значительно замедлится. На самом деле я напечатал приведенное выше введение более месяца назад. Теперь, чтобы начать, я буду работать над грамматикой, которую мы разработали ранее. В конце концов, я создам текстовый файл в реальном проекте, который описывает грамматику. А пока я просто воспользуюсь написанной мной статьей.

Хороший способ начать создание языка — сначала реализовать основные функции. Мы создадим основу для нашего проекта, и мы создадим некоторые арифметические функции, которые есть в нашем языке. Итак, начнем.

Базовый терминальный интерфейс

Для начала заставим Sea работать как из терминала, аналогично команде python, так и для файлов. Я начал с создания простого интерфейса терминала, который выводит то, что вводит пользователь, и завершается, если пользователь вводит exit, ^C (прерывание клавиатуры) или ^D (конец файла):

В результате интерфейс будет вести себя следующим образом:

Чтобы это работало, нам нужно продолжать развивать наш main.py:

Я использую рекомендуемый подход в Python 3 для использования if __name__ == "__main__", поэтому мой код находится внутри основной функции. Здесь многое просто запланировано на потом. Первые две строки функции main просто анализируют некоторые аргументы командной строки, которые мы будем использовать позже. mode сообщит нам, следует ли использовать транспилятор, компилятор, интерпретатор файлов или интерпретатор терминала. debug сообщит нам, печатать отладочную информацию или нет. Остальные переданные аргументы являются дополнительной информацией, описывающей файлы, которые мы в конечном итоге прочитаем.

На данный момент у нас подключен только наш простой терминал. Чтобы запустить это, нам нужно запустить python modules/main.py None False. Хотя мы еще не используем аргумент debug, программа ожидает как минимум два аргумента. В будущем мы создадим программу bash, чтобы упростить работу с нашим языком. Обратите внимание, что в каждом из этих разделов статьи я буду ссылаться на коммит, о котором я говорю. Если вы хотите увидеть полную структуру проекта в любой момент времени, ее можно найти здесь. Также обратите внимание, что в этих сегментах кода GitHub я буду добавлять суффикс к имени файла с фиксацией, из которой этот файл.

Базовый общий интерфейс

Как упоминалось ранее, нам нужен как файловый интерфейс, так и терминальный интерфейс. Итак, мы можем создать общий интерфейс, который позволяет работать обоим. Для начала мы должны сформировать основу нашей обработки ошибок. Существует множество способов обработки ошибок лексера, синтаксического анализатора и посетителей; однако я решил использовать для этого исключения Python. Поскольку серьезная ошибка помешает завершению нашего процесса, исключения в этом контексте абстрактно имеют смысл.

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

Пока это наш общий интерфейс. Вы должны узнать debug и mode из предыдущего. Я также добавил этот параметр streams. Это связано с тем, что при взаимодействии через терминал весь ввод и вывод должен выполняться одним способом, а при взаимодействии через файлы весь ввод и вывод должен выполняться другим способом. Передав этой функции общий объект streams, который мы вскоре определим, оба этих типа интерфейсов могут работать здесь. Также обратите внимание, что когда отладка включена, независимо от того, есть ошибка или нет, информация об отладке будет напечатана.

Потоки

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

Сначала это может показаться ошеломляющим, но я обещаю вам, что это не так уж сложно. В streams/general.py я создал три абстрактных класса типов потока. InStream — это наш абстрактный класс входного потока, который просто имеет метод для чтения потока по одному символу за раз. OutStream — это наш абстрактный поток вывода, который просто записывает данные в поток. ErrorStream — это наш абстрактный поток ошибок, который представляет собой модифицированный поток вывода, который может записывать ошибку. Затем я также создал файл NullStream. Это поток, который можно использовать для ввода, вывода или ошибок, который просто ничего не делает.

Далее идет holder.py, который создает простой класс данных — классы данных — это очень полезная функция Python, о которой вам следует узнать, — который содержит четыре потока. Этот объект-держатель — это то, что мы передадим в interface как streams в interfaces/general.py.

Наконец, мы реализуем эти потоки для терминального интерфейса. Мы будем читать символы из буфера, который заполняется interfaces/terminal.py. Вывод и ошибки нужно только распечатать на терминале.

Таким образом, хотя на первый взгляд весь этот код может показаться большим, на самом деле это какой-то невероятно простой код, который служит в основном для создания этой абстрактной концептуализации потоков.

Использование общего интерфейса

Чтобы использовать наш общий интерфейс, нам нужно связать его с нашим терминальным интерфейсом.

Для начала я переименовал функцию begin_interfacing в interface. Не забудьте также изменить main.py, чтобы отразить это. Теперь мы начинаем взаимодействовать с терминалом, создавая наш объект streams (строки с 8 по 13). Используя наши ранее созданные классы, эта задача проста. В строке 17 наш буфер теперь также записывает в наш входной объект. Причина, по которой у нас вообще есть локальная переменная buffer, связана с планами на будущее. Это также помогает нам упростить наш код. Я также добавил несколько проверок ввода. Если пользователь ничего не вводит, цикл просто предложит ему снова. Кроме того, я создал способ, с помощью которого пользователь может включать и выключать режим отладки, пока программа работает в терминальном режиме. Как только мы создадим буфер внутри объекта streams.in_stream, мы готовы передать объект general.interface.

Для нашего файла general.py все, что я сделал, это модифицировал наш блок try, чтобы он выполнял ту же функцию, что и раньше: вывод пользовательского ввода прямо ему. Если вы следуете структуре проекта, я также изменил наш Makefile, чтобы мы могли просто запустить make вместо python modules/main.py None False. Тестирование нашего кода во время написания имеет решающее значение для поиска ошибок до того, как они выйдут из-под контроля.

Позиции

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

Класс SymbolPosition отслеживает строку и столбец, в которых появляется символ, при заданном потоке. Затем класс Position отслеживает часть символов в потоке. Оба этих класса имеют несколько полезных вспомогательных методов, которые мы будем использовать позже. Затем я также обновил наш класс ErrorStream, так что печатное сообщение теперь будет f”{type(data).__name__} at {data.position}: {data.get_message()}\n”. Наконец, я изменил класс SeaError, чтобы он также имел переменную экземпляра position:

Теперь, когда у нас есть механизм хранения позиций, давайте посмотрим, как они используются в лексере.

Лексер Скелет

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

Я реализовал все лексические операции, которые нам понадобятся, за исключением фактического построения токена. Наш лексер будет иметь текущую позицию во входном потоке, которую он отслеживает, чтобы наши токены могли иметь точную позицию. Затем мы можем использовать наши методы skip, advance и take, чтобы переходить символ за символом для создания токенов. Как только мы реализуем метод take_token, наш лексер заработает. Я также создал фундаментальный LexerError, на основе которого мы можем строить ошибки. Теперь нам нужны токены для создания.

Жетоны

Возвращаясь к моей предыдущей статье, у нас будут токены keyword, identifier, constant, string-literal и punctuator. Со временем мы все это реализуем. Однако пока мы только пытаемся реализовать нашу арифметическую функциональность. Итак, нам нужны только константы и знаки препинания. В частности, мы создадим следующие классы: Token, Constant, NumericalConstant, Punctuator и Operator.

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

Когда я пишу это, я понял, что это пугающе большая задача, и поэтому я решил разделить эту статью (по крайней мере) на две части. В следующей статье я продолжу развитие нашего лексера. Мы сможем создавать токены и распечатывать список токенов в отладочной информации. Мы создадим синтаксический анализатор для создания абстрактного синтаксического дерева, а также наш интерпретатор. Ближе к концу мы сможем читать из файлов, транспилировать код в C и создадим bash-программу для взаимодействия с нашим кодом. На этом арифметический раздел Моря завершится. Кроме того, мы продолжим всю необходимую работу над этим языком. Если у вас есть вопросы, задавайте их.