Я давно хотел иметь свой собственный язык программирования, который облегчит создание текстовых приключенческих игр для моего Open Source проекта jQuery Terminal. Идея языка возникла после того, как я создал платное задание для одного человека, назовем его Кен, которому нужна была игра такого типа, где пользователь взаимодействовал с терминалом и ему задавали кучу вопросов, и это было похоже на приключенческую игру, связанную с Crypo. Код, который я написал и который был нужен Кену, управлялся данными из файла JSON. Он работал хорошо, Кен мог легко изменить JSON и заставить игру измениться, как он хотел. Я спросил, могу ли я поделиться кодом, поскольку это был очень крутой проект, и Кен согласился, что я могу сделать это через два месяца после публикации игры. Но через некоторое время я понял, что могу получить нечто гораздо лучшее. Мой собственный язык DSL, который упростит создание текстовых приключенческих игр. Человек с небольшими знаниями программирования, как Кен, сможет легко редактировать игру, потому что язык будет намного проще, чем сложный код JavaScript, который необходим для чего-то подобного. И даже если бы меня попросили создать игру, подобную той, что была создана для Кена, мне было бы гораздо проще и быстрее. Так началось создание языка программирования Gaiman.
До этого я использовал PEG.js, поэтому он был моим очевидным выбором для генератора парсера. Сначала я начал с арифметического примера, изменил его, а затем добавил оператор if и булевы выражения. Когда у меня получилось первое доказательство концепции, которое генерировало выходной код JavaScript, я был так взволнован, что мне пришлось написать статью и поделиться тем, как просто создать свой собственный язык программирования на JavaScript.
В конце есть простая демонстрационная площадка, а если вы хотите чего-то более крутого, загляните на сайт Gaiman, ссылка на GitHub.
Итак, к делу, давайте погрузимся.
- Что такое компилятор?
- Что такое парсер?
- Что такое AST?
- Что такое генератор парсера?
- Генерация кода JavaScript
- Простой пример парсера PEG.js
- Оператор if
- условие оператора if
- Переменные
- Буквальная строка
- Блок с фигурными скобками
- Целая программа
- Парсер PEG для вашего собственного языка, компилируемого в JavaScript
- Генерация кода JavaScript
- Заключение
Что такое компилятор?
Компилятор – это программа, которая переводит код с одного языка программирования на другой. Например, компилятор языка C переводит программу, написанную на языке C, в машинный код (двоичный код, который может быть интерпретирован компьютером). Но существуют также компиляторы, которые переводят один человекочитаемый язык в другой читаемый язык. Например, ClojureScript компилируется в JavaScript. Этот процесс часто называют транспилингом, а программу, которая это делает, – транспилером.
Что такое парсер?
Парсер – это программа, которая может быть частью компилятора или интерпретатора. Он принимает входной код в виде последовательности символов и создает AST (абстрактное синтаксическое дерево), которое может быть использовано генератором кода (часть компилятора) для создания выходного кода или оценщиком (часть интерпретатора) для его выполнения.
Что такое AST?
AST – это аббревиатура от Abstract Syntax Tree. Это способ представления кода в формате, понятном инструментам. Обычно в виде древовидной структуры данных. Мы будем использовать AST в формате Esprima, который является парсером JavaScript, выводящим AST.
Что такое генератор парсера?
Генератор парсера, как следует из названия, – это программа, которая генерирует для вас исходный код парсера на основе грамматики (спецификации языка). Она написана в определенном синтаксисе. В этой статье мы будем использовать генератор парсера PEG.js, который генерирует код JavaScript, который будет разбирать код для вашего языка и выводить AST.
Генератор парсера также является компилятором, поэтому его можно назвать компилятором компиляторов. Компилятор, который может вывести компилятор для вашего языка.
Генерация кода JavaScript
Что самое интересное в синтаксисе Esprima, так это то, что существуют инструменты, которые генерируют код на основе его AST. Примером может служить escodegen, который принимает на вход Esprima AST и выдает код JavaScript. Вы можете подумать, что для генерации кода можно использовать просто строки, но это решение не будет масштабироваться. В этом руководстве я показываю только один оператор if, но вы столкнетесь с множеством проблем, если у вас будет более сложный код.
Простой пример парсера PEG.js
PEG.js – это компилятор для разбора грамматик выражений, написанный на JavaScript. Он берет более простой язык PEG, который использует встроенный код JavaScript, и выводит парсер.
Ниже я покажу, как создать простой парсер PEG.js грамматики для оператора if, который будет выводить AST, который затем будет преобразован в код JavaScipt.
Синтаксис PEG.js не очень сложен, он состоит из названия правила, затем совпадающего и необязательного блока JavaScript, который выполняется и возвращается из правила.
Вот простой арифметический пример, приведенный в документации PEG.js:
{
function makeInteger(o) {
return parseInt(o.join(""), 10);
}
}
start
= additive
additive
= left:multiplicative "+" right:additive { return left + right; }
/ multiplicative
multiplicative
= left:primary "*" right:multiplicative { return left * right; }
/ primary
primary
= integer
/ "(" additive:additive ")" { return additive; }
integer "integer"
= digits:[0-9]+ { return makeInteger(digits); }
Выходной парсер этой грамматики может разбирать и оценивать простые арифметические выражения, например, 10+2*3, которое оценивается в 16. Вы можете протестировать этот парсер в PEG.js Online Tool. Обратите внимание, что он не обрабатывает пробелы между лексемами (для упрощения кода), в парсере это нужно делать явно.
Но нам нужно не интерпретировать код и вернуть одно значение, а вернуть Esprima AST. Чтобы посмотреть, как выглядит Esprima AST, вы можете проверить AST Explorer, выбрать Esprima в качестве вывода и набрать немного JavaScript.
Вот пример такого простого кода:
if (foo == "bar") {
10 + 10
10 * 20
}
Вывод в формате JSON выглядит следующим образом:
{
"type": "Program",
"body": [
{
"type": "IfStatement",
"test": {
"type": "BinaryExpression",
"operator": "==",
"left": {
"type": "Identifier",
"name": "foo",
"range": [
4,
7
]
},
"right": {
"type": "Literal",
"value": "bar",
"raw": ""bar"",
"range": [
11,
16
]
},
"range": [
4,
16
]
},
"consequent": {
"type": "BlockStatement",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Literal",
"value": 10,
"raw": "10",
"range": [
23,
25
]
},
"right": {
"type": "Literal",
"value": 10,
"raw": "10",
"range": [
28,
30
]
},
"range": [
23,
30
]
},
"range": [
23,
30
]
},
{
"type": "ExpressionStatement",
"expression": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Literal",
"value": 10,
"raw": "10",
"range": [
34,
36
]
},
"right": {
"type": "Literal",
"value": 20,
"raw": "20",
"range": [
39,
41
]
},
"range": [
34,
41
]
},
"range": [
34,
41
]
}
],
"range": [
18,
43
]
},
"alternate": null,
"range": [
0,
43
]
}
],
"sourceType": "module",
"range": [
0,
43
]
}
Вам не нужно заботиться о “range” и “raw”. Они являются частью вывода парсера.
Давайте разделим JSON на части:
Оператор if
Оператор if должен быть в формате:
{
"type": "IfStatement",
"test": {
},
"consequent": {
},
"alternate": null
}
Где “test” и “consequent” – любые выражения:
условие оператора if
Условие может быть любым выражением, но здесь у нас будет бинарное выражение, которое сравнивает две вещи:
{
"type": "BinaryExpression",
"operator": "==",
"left": {},
"right": {}
}
Переменные
Использование переменных выглядит следующим образом:
{
"type": "Identifier",
"name": "foo"
}
Буквальная строка
Буквальная строка, используемая в нашем коде, выглядит следующим образом:
{
"type": "Literal",
"value": "bar"
}
Блок с фигурными скобками
Блок внутри if создается следующим образом:
{
"type": "BlockStatement",
"body": [ ]
}
Целая программа
Вся программа создается следующим образом:
{
"type": "Program",
"body": [ ]
}
Парсер PEG для вашего собственного языка, компилируемого в JavaScript
Для нашего демонстрационного языка мы создадим код, похожий на ruby:
if foo == "bar" then
10 + 10
10 * 20
end
и создадим AST, который затем создаст код JavaScript.
Грамматика Peg для if выглядит следующим образом:
if = "if" _ expression:(comparison / expression) _ "then" body:(statements / _) _ "end" {
return {
"type": "IfStatement",
"test": expression,
"consequent": {
"type": "BlockStatement",
"body": body
},
"alternate": null
};
}
У нас есть лексема “if”, затем выражение, которое является сравнением или выражением, а тело – это утверждения или пробелы. _ – это необязательные пробелы, которые игнорируются.
_ = [ tnr]*
Сравнение выглядит следующим образом:
comparison = _ left:expression _ "==" _ right:expression _ {
return {
"type": "BinaryExpression",
"operator": "==",
"left": left,
"right": right
};
}
Выражение выглядит следующим образом:
expression = expression:(variable / literal) {
return expression;
}
Переменная создается из трех правил:
variable = !keywords variable:name {
return {
"type": "Identifier",
"name": variable
}
}
keywords = "if" / "then" / "end"
name = [A-Z_$a-z][A-Z_a-z0-9]* { return text(); }
Теперь давайте рассмотрим утверждения:
statements = _ head:(if / expression_statement) _ tail:(!"end" _ (if / expression_statement))* {
return [head].concat(tail.map(function(element) {
return element[2];
}));
}
expression_statement = expression:expression {
return {
"type": "ExpressionStatement",
"expression": expression
};
}
И последнее – это литералы:
literal = value:(string / Integer) {
return {"type": "Literal", "value": value };
}
string = """ ([^"] / "\\"")* """ {
return JSON.parse(text());
}
Integer "integer"
= _ [0-9]+ { return parseInt(text(), 10); }
Генерация кода JavaScript
Вот и весь парсер, который генерирует AST. После того, как у нас есть Esprima AST, нам остается только сгенерировать код с помощью escodegen.
Код, который генерирует AST и создает код JavaScript, выглядит следующим образом:
const ast = parser.parse(code);
const js_code = escodegen.generate(ast);
Переменная parser – это имя, которое вы даете при генерации парсера с помощью PEG.js.
Вот простая демонстрация, которую я использовал для написания парсера, вы можете поиграть с грамматикой и сгенерировать другой синтаксис для вашего собственного языка программирования, который компилируется в JavaScript.
Parser Generator Demo.
Это простое приложение сохраняет ваш код в LocalStorage, если он компилируется без ошибок, при каждом изменении. Поэтому вы можете смело использовать его для создания своего собственного языка. Но я не гарантирую, что вы не потеряете свою работу, поэтому вы можете использовать что-то более надежное.
ПРИМЕЧАНИЕ: Оригинальный проект PEG.js больше не поддерживается, но есть новый форк, Peggy, который поддерживается и обратно совместим с PEG.js, так что перейти на него будет легко.
Если вы хотите что-то более продвинутое, вы можете посмотреть на Gaiman Programming Language Playground, если вы включите режим dev, то сможете редактировать грамматику и видеть выходной JavaScript. У вас также есть модуль AST, где вы можете увидеть, что является выводом AST данного кода JavaScript. В этом демо используется Peggy.
Заключение
В этой статье мы использовали генератор парсеров для создания простого пользовательского языка для компилятора JavaScript. Как вы видите, начать подобный проект не так уж и сложно. Приемы, описанные в этой статье, позволят вам самостоятельно создать любой язык программирования, компилируемый в JavaScript. Это может быть способом создания PoC языка, который вы хотите разработать. Насколько я знаю, это самый быстрый способ получить что-то работающее. Но вы можете использовать язык как есть, создать свой собственный DLS (Domain Specific Language), писать код на этом языке и заставить JavaScript делать тяжелую работу.
Если вам понравился этот пост, вы можете следить за мной в twitter по адресу @jcubic и зайти на мою домашнюю страницу.
А здесь вы можете найти несколько вакансий на JavaScript.