Как создать свой собственный язык программирования на JavaScript

Я давно хотел иметь свой собственный язык программирования, который облегчит создание текстовых приключенческих игр для моего Open Source проекта jQuery Terminal. Идея языка возникла после того, как я создал платное задание для одного человека, назовем его Кен, которому нужна была игра такого типа, где пользователь взаимодействовал с терминалом и ему задавали кучу вопросов, и это было похоже на приключенческую игру, связанную с Crypo. Код, который я написал и который был нужен Кену, управлялся данными из файла JSON. Он работал хорошо, Кен мог легко изменить JSON и заставить игру измениться, как он хотел. Я спросил, могу ли я поделиться кодом, поскольку это был очень крутой проект, и Кен согласился, что я могу сделать это через два месяца после публикации игры. Но через некоторое время я понял, что могу получить нечто гораздо лучшее. Мой собственный язык DSL, который упростит создание текстовых приключенческих игр. Человек с небольшими знаниями программирования, как Кен, сможет легко редактировать игру, потому что язык будет намного проще, чем сложный код JavaScript, который необходим для чего-то подобного. И даже если бы меня попросили создать игру, подобную той, что была создана для Кена, мне было бы гораздо проще и быстрее. Так началось создание языка программирования Gaiman.

До этого я использовал PEG.js, поэтому он был моим очевидным выбором для генератора парсера. Сначала я начал с арифметического примера, изменил его, а затем добавил оператор if и булевы выражения. Когда у меня получилось первое доказательство концепции, которое генерировало выходной код JavaScript, я был так взволнован, что мне пришлось написать статью и поделиться тем, как просто создать свой собственный язык программирования на JavaScript.

В конце есть простая демонстрационная площадка, а если вы хотите чего-то более крутого, загляните на сайт Gaiman, ссылка на GitHub.

Итак, к делу, давайте погрузимся.

Что такое компилятор?

Компилятор – это программа, которая переводит код с одного языка программирования на другой. Например, компилятор языка 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); }
Enter fullscreen mode Выйти из полноэкранного режима

Генерация кода 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.

Оцените статью
Procodings.ru
Добавить комментарий