Всем привет! В прошлый раз мы представили PizzaScript - образовательный Go-проект с открытым исходным кодом. Мы продолжаем эту статью, и встреча состоится 13 февраля, 10:00 CET. В этих материалах мы делаем обзор проекта и знакомим с ключевыми концепциями создания нового языка программирования. Мы покажем реализацию простого калькулятора с использованием Golang
и RxGo
. Эта статья посвящена lexer’s
сценарию с ReactiveX
, оставив parser
и eval
на потом. Эта серия статей может помочь новичкам в изучении Golang
и WebAssembly
и экспериментировать с живым проектом с открытым исходным кодом.
- I Slice - Введение в PizzaScript
- II Slice - Языки программирования
- III срез - реактивные паттерны
Я разрезаю
- который знакомит с языком программирования PizzaScript, также известным как
испорченный язык программирования ©
PizzaScript
или ps
- новый крутой язык с необычной парадигмой.
Почему? Я люблю JavaScript
💛 и TypeScript
💙. Мне особенно нравятся JavaScript
причуды, и я всегда хотел больше свободы, чем дает JavaScript
язык.
И я еще не пробовал создать свой собственный язык. Я решил это сделать, и мои цели:
- Понять, как работают языки программирования и интерпретаторы
- Изучите
Go
язык и ключевые библиотеки, напримерRxGo
- Экспериментируйте с
WebAssembly
Кроме того, в любом случае здорово создать новый язык и компилятор.
PizzaScript 🍕 Основные характеристики
- Классное название (и логотип, todo)!
Это, пожалуй, самая важная из всех функций. Мы гордимся своим выбором.
JavaScript
-подобный синтаксис, своего рода- Переменные, целые числа, числа с плавающей запятой и строки
- Арифметические выражения, встроенные функции
- Первоклассные функции, замыкания
- Динамические типы и приведения
- Модули и стандартная библиотека
- Потрясающие идеи и поток интерпретаторов
var h1679 = "1" val g2788 = 2 h1679 + g2788 == "12" g2788 + h1679 == 3
PizzaScript
компилируется вWebAssembly
, что делает его переносимой и подходящей целью для выполнения как на стороне клиента, так и на стороне сервера.
Хорошо, звучит сложно. Как мы его построим?
- Я сам заинтересован в изучении
Go
языка. - Я тоже хотел разложить проект a
reactive
способом. Кроме того, изучение библиотеки может помочь в освоении нового языка программирования. RxGo поможет организовать поток данных - разбить вводимый текст на токены, затем проанализировать и организовать их в структуру данных абстрактно-синтаксическое дерево.
WebAssembly
- это большое дело, и мы собираемся производить модули в форматахwasm
илиwat
.
II Slice - Языки программирования
Если вы спросите меня, язык программирования - это произведение искусства 😀. Серьезно, это огромная работа, чтобы понять, создать и поддержать его. Лично мое первое представление о языке - это образцы кода, написанные на нем. Как и в случае с PizzaScript
, в этом небольшом примере показано определение функции
fun sum(var a1573: string, b7232): int { a1573 + b7232 } sum(1, 2) // 12
Как видите, функции объявляются с ключевым словом fun
.
Что ж, это не совсем весело 🤔
Итак, что такое язык программирования? Здесь, безусловно, нужно дать несколько определений:
- У нас есть
alphabet
всех доступных символов. Написанный на нем текст по сути представляет собой строку или цепочку символов. - А затем у нас есть конкретный набор цепочек из
alphabet
. Те образуютlanguage
.
Вопрос - как указать язык? И, конечно, есть разные способы сделать это. Самый простой - перечислить в нем все цепочки. Как можно себе представить, большинство языков программирования состоят из бесконечного числа цепочек.
То есть, я с трудом могу представить, чтобы кто-то написал хотя бы две одинаковые программы на
PizzaScript
🤷♂️
Нам нужен другой способ, и это обычно схематическое представление, подразумевающее алгоритм для вывода всех потенциальных цепочек. Может быть:
- программа, которая проверяет, принадлежит ли данный текст к определенному языку,
- или программа, которая генерирует возможные цепочки из языка
Grammar
- это набор правил, определяющих язык, записанный в специальной форме.
Наиболее известна форма Backus–Naur
.
<personal-part> ::= <initial> "." | <first-name>
Считайте каждый <personal-part>
как <initial>
с типами <.>
или <first-name>
символьных последовательностей, разделенных |
. Ниже вы можете увидеть простой язык калькулятора в форме BNF.
expr ::= operand {('+' | '-' | '*' | '/') operand} ; any number of infix operations
; with no precedence
operand ::= num [+/-] | '(' expr ')' ; a number followed optionally by
; +/- or a parenthsized expression
num ::= digit {digit} ['.' {digit}]] ; numbers can be integer or real
digit ::= '0 | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
Популярный генератор парсеров
antlr
предоставляет расширенную версию грамматики калькулятора.
Вообще говоря, эти типы грамматики могут быть переведены в компьютерные программы. А программа, которую мы собираемся построить, - это компилятор.
Итак, как работают компиляторы языков программирования?
Этой теме посвящено множество исследований, и мы лишь вкратце охватим основные идеи. Обычно программа компилятора состоит из последовательных, но независимых этапов. У каждого своя цель.
Лексический анализ отвечает за разбиение входного текста на подпоследовательности лексем. Он фильтрует разделители (например, пробелы или ;
) и преобразует значимые символы в типизированные объекты, называемые токенами. Токены бывают разных типов, например, типы данных - числа, строки, определения переменных - идентификаторы, операторы - +, -, =, / и т. Д.
Синтаксический анализ или парсеры работают с набором лексем, переданным с первого этапа. У него две цели - проверить, соответствует ли программа синтаксису, и создать декларативное представление для дальнейшей обработки.
Результатом этого этапа является абстрактное синтаксическое дерево (ast). Это объект данных, представляющий текст программы в виде древовидной структуры. Каждый узел в дереве обозначает конструкцию языка, происходящую в исходной программе.
Формат ast не имеет единого стандарта для разных языков. Например, babel - популярный JavaScript
transpiler (еще один интересный термин, часто используемый в контексте компилятора), который преобразует разные EcmaScript
версии друг в друга, Имеет свою определенную спецификацию.
Это пример типов узлов, определенных в формате babel ast
.
interface Node { type: string; loc: SourceLocation | null; } interface NumericLiteral <: Literal { type: "NumericLiteral"; value: number; }
AST Explorer позволяет нам увидеть, как разные языки могут быть представлены в разных
ast
форматах
И есть разные сценарии, которые происходят после разбора программы. Например, он может быть скомпилирован в целевые инструкции, оценен или интерпретирован.
Целью компиляции PizzaScript
является WebAssembly
, что означает, что мы будем производить вывод в формате wasm
или wat
. Наша цель - сделать переносимый язык программирования.
III срез - реактивные паттерны
А пока мы обсудили этапы обработки текста программы, а именно:
- для разбиения текста на токены или для лексического анализа,
- для разбора лексем или синтаксического анализа,
- построить абстрактное синтаксическое дерево,
- для компиляции или оценки проанализированного дерева.
Эти фазы могут работать независимо и давать результат для каждого шага. В качестве альтернативы данные могут передаваться между фазами однонаправленно. Синтаксический анализатор может запросить новый токен у лексического анализатора, или лексер может создать токен после того, как он будет идентифицирован.
Одна из идей, которую я всегда преследовал, заключалась в том, чтобы разложить приложение на реактивные шаблоны. В этой парадигме все данные представлены как поток асинхронных событий. И проект ReactiveX
прекрасно объединяет связанные шаблоны, такие как Observable, Iterator, Functional Programming
. Он позволяет использовать акторов и операторов API для объединения, преобразования и разделения потоков.
Мы выбрали Golang
и будем использовать RxGo
реализацию ReactiveX
. Он имеет достаточную поддержку своих концепций и операторов.
Начнем с варианта использования калькулятора, и мы собираемся создать:
main
- основной запускаемый пакет,repl
- считывает ввод пользователя, инициализирует всех необходимых актеров и производит вывод,token
- объявляет разрешенные символы и операторыPizzaScript
,lexer
- разбивает вводимый текст на токены,parser
- генерирует дерево абстрактного синтаксиса программы,ast
- описываетast
типы,eval
- оценивает дерево программы и выдает результат.
Одна очень важная вещь, которую я еще не сказал. Этот проект глубоко вдохновлен Writing An Interpreter In Go by Thorsten Ball, но не только. Есть много интересных ресурсов, которыми я хотел бы поделиться во время следующих статей и мероприятий. Некоторые примеры взяты из книги и переписаны с учетом нашего интереса.
Начнем с пакета main
. Он просто запускает repl
общедоступный метод Start.
Пакет Repl
или Read-Eval-Print-Loop implementation
довольно прост.
Это цикл, который взаимодействует с пользователем. Он читает ввод и печатает вывод. Ха, давай уже запустим
$ git clone https://github.com/x-technology/PizzaScript.git $ cd pizzascript $ go get $ go run main.go Hello alex! This is the PizzaScript programming language! Feel free to type in commands >> 123 123 >> 1+2 3 >> 2+3 5 >> 123+2 125 >> 12*3 36
Вау, а работает ?! Как уже было сказано, мы не увидим здесь парсер и eval. Но давайте проверим самые интересные на сегодня срезы - token
и lexer
.
Token
описывает доступные токены
Все идет нормально. Давайте также представим пакет lexer
lexer
(или токенизатор) - вот где мы собираемся использовать асинхронные потоки событий. Почему? У нас есть набор стандартных операторов, итераторов и потоков, определенных во фреймворке. И похоже, что приложение можно представить в виде цепочек асинхронных токенов и потоков узлов.
Но это скорее предположение, мы рассмотрим эту тему на будущих сессиях.
Мы создаем и сохраняем Observable, созданный на основе ввода текста, и разбиваем его на отдельные символы.
Lexer
должен фильтровать бессмысленные символы. С PizzaScript
s p a c e s 👽, конец строк, вкладки можно пропустить.
Оператор фильтра довольно прост - для заданного потока данных он выполняет функцию обратного вызова для каждого события и возвращает значение bool
, чтобы указать, должны ли данные оставаться в потоке вывода.
Следующий шаг - объединить некоторые символы в жетоны. Примером может быть число из нескольких символов 125
, что нормально. То же и с идентификаторами. Мы еще не представляем их, но это место в коде, куда нам нужно добавить их в будущем. Вот где это становится немного сложнее. С шаблоном Observable
мы перебираем асинхронные события. В идеале мы стремимся не иметь побочных эффектов и изолировать нашу область видимости только от обратных вызовов операторов.
Оператор Scan
во многих аспектах похож на map + reduce
в приведенном выше коде. Аргумент первого обратного вызова оператора - это значение аккумулятора, возвращенное с предыдущей итерации, и результат передается в новый поток на каждой итерации. Это наш способ использования здесь техники look-ahead
- мы сохраняем значение предыдущей итерации и на его основе создаем новое. Введем промежуточный тип состояния. Его цель - собрать символы в жетоны, если у них есть соответствующий атрибут. В нашем случае числа объединяются. Поскольку у нас еще нет идентификаторов, реализация почти завершена.
Наконец, давайте отфильтруем неразрешенные промежуточные состояния, созданные на этапе Scan
, и сопоставим их с токенами.
На этом наша первая попытка завершена.
Следующие шаги
В эту субботу, 13 февраля, 10:00 по центральноевропейскому времени, мы встретимся на живом заседании, чтобы узнать подробности по этому поводу. Приглашаем вас присоединиться! В следующей статье мы рассмотрим функции parser
и eval
. Собственно, спрашиваем ваше мнение по этому поводу.
Какой должна быть наша следующая тема для PizzaScript?
- Подробнее о PizzaScript 🍕
- Глубоко в парсеры, языки программирования 🎓
- Давай уже скомпилируем его до
WebAssembly
💻
Дайте нам знать в комментариях! 🙏
Мы только начали, не стесняйтесь присоединяться к нам:
- Твиттер
- "YouTube"
- Eventbrite
- "Телеграмма"
И, конечно же, исходный код открыт. Мы высоко ценим любые отзывы, вклад или помощь.
Спасибо за уделенное время и приятного кодирования!