Как бы я расширил язык JavaScript для поддержки нового оператора?

Ответ на вопрос Можно ли создавать пользовательские операторы в JavaScript? пока нет, но @Benjamin предположил, что можно было бы добавить новый оператор, используя сторонние инструменты:

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

Я возьму тот же пример, как и в предыдущем вопросе:

(ℝ, ∘), x ∘ y = x + 2y

Для любых двух действительных чисел x и y: x ∘ y равно x + 2y, что также является действительным числом. Как я могу добавить этот оператор в моем расширенном языке JavaScript?

После запуска следующего кода:

var x = 2
  , y = 3
  , z = x ∘ y;

console.log(z);

Выход будет содержать

8

(так как 8 является 2 + 2 * 3)


Как бы я расширил язык JavaScript для поддержки нового оператора?

2 ответа

Решение

Да, это возможно и даже не очень сложно:)


Нам нужно обсудить несколько вещей:

  1. Что такое синтаксис и семантика.
  2. Как анализируются языки программирования? Что такое синтаксическое дерево?
  3. Расширение синтаксиса языка.
  4. Расширение языковой семантики.
  5. Как добавить оператор в язык JavaScript.

Если вы ленивы и просто хотите увидеть это в действии - я разместил рабочий код на GitHub

1. Что такое синтаксис и семантика?

В целом - язык состоит из двух вещей.

  • Синтаксис - это символы в языке, такие как унарные операторы, такие как ++, так же как Expression это как FunctionExpression которые представляют "встроенную" функцию. Синтаксис представляет только используемые символы, а не их значение. Короче говоря, синтаксис - это просто рисунки из букв и символов - он не имеет внутреннего значения.

  • Семантика связывает значение с этими символами. Семантика это то, что говорит ++ означает "увеличение на единицу", на самом деле здесь точное определение. Это связывает значение с нашим синтаксисом, и без него синтаксис представляет собой просто список символов с порядком.

2. Как анализируются языки программирования? Что такое синтаксическое дерево?

В какой-то момент, когда что-то выполняет ваш код на JavaScript или любом другом языке программирования - он должен понимать этот код. Часть этого, называемая лексингом (или токенизацией, давайте не будем здесь вдаваться в тонкие различия), означает разбивку кода, например:

function foo(){ return 5;}

В его значимые части - это говорит о том, что есть function здесь ключевое слово, за которым следует идентификатор, пустой список аргументов, затем открытие блока { содержащий ключевое слово return с литералом 5 затем точка с запятой, затем конечный блок },

Эта часть полностью в синтаксисе, все, что он делает, это разбивает его на такие части, как function,foo,(,),{,return,5,;,}, У него все еще нет понимания кода.

После этого - Syntax Tree построено. Синтаксическое дерево лучше знает грамматику, но все еще полностью синтаксическое. Например, синтаксическое дерево будет видеть токены:

function foo(){ return 5;}

И понять: "Эй! Здесь есть объявление функции!".

Это называется деревом, потому что оно просто так - деревья допускают вложение.

Например, приведенный выше код может создать что-то вроде:

                                        Program
                                  FunctionDeclaration (identifier = 'foo')
                                     BlockStatement
                                     ReturnStatement
                                     Literal (5)

Это довольно просто, просто чтобы показать вам, что это не всегда так линейно, давайте проверим 5 +5:

                                        Program
                                  ExpressionStatement
                               BinaryExpression (operator +)
                            Literal (5)       Literal(5)   // notice the split her

Такие расколы могут произойти.

По сути, синтаксическое дерево позволяет нам выразить синтаксис.

Это где x ∘ y не удается - он видит и не понимает синтаксис.

3. Расширение синтаксиса языка.

Это просто требует проекта, который анализирует синтаксис. Здесь мы будем читать синтаксис "нашего" языка, который не совпадает с JavaScript (и не соответствует спецификации), и заменять наш оператор чем-то, с чем хорошо работает синтаксис JavaScript.

Мы будем делать не JavaScript. Он не соответствует спецификации JavaScript, и JS-анализатор жалобы на стандарты выдаст исключение.

4. Расширение языковой семантики

Так или иначе, мы делаем это все время:) Все, что мы сделаем здесь, это просто определим функцию, которая будет вызываться при вызове оператора.

5. Как добавить оператор в язык JavaScript.

Позвольте мне начать с того, что после этого префикса мы не будем добавлять здесь оператор JS, скорее - мы определяем наш собственный язык - давайте назовем его "CakeLanguage" или что-то в этом роде и добавим оператор в него. Это потому что не является частью грамматики JS, и грамматика JS не допускает произвольных операторов, как некоторые другие языки.

Мы будем использовать два проекта с открытым исходным кодом для этого:

  • esprima, которая принимает код JS и генерирует для него дерево синтаксиса.
  • escodegen, который делает другое направление, генерируя JS-код из синтаксического дерева esprima spits.

Если бы вы обратили пристальное внимание, вы бы знали, что мы не можем использовать esprima напрямую, так как мы дадим грамматику, которую он не понимает.

Мы добавим # оператор, который делает x # y === 2x + y для удовольствия. Мы дадим ему приоритет множественности (потому что операторы имеют приоритет операторов).

Итак, после того, как вы получите свою копию Esprima.js - нам нужно изменить следующее:

к FnExprTokens - это выражения нам нужно добавить # таким образом, это признало бы это. После этого это будет выглядеть так:

FnExprTokens = ['(', '{', '[', 'in', 'typeof', 'instanceof', 'new',
                    'return', 'case', 'delete', 'throw', 'void',
                    // assignment operators
                    '=', '+=', '-=', '*=', '/=', '%=', '<<=', '>>=', '>>>=',
                    '&=', '|=', '^=', ',',
                    // binary/unary operators
                    '+', '-', '*', '/', '%','#', '++', '--', '<<', '>>', '>>>', '&',
                    '|', '^', '!', '~', '&&', '||', '?', ':', '===', '==', '>=',
                    '<=', '<', '>', '!=', '!=='];

к scanPunctuator мы добавим его и его код в качестве возможного случая: case 0x23: // #

А потом к тесту так это выглядит так:

 if ('<>=!+-*#%&|^/'.indexOf(ch1) >= 0) {

Вместо:

    if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) {

А потом binaryPrecedence давайте дадим ему тот же приоритет, что и множественность:

case '*':
case '/':
case '#': // put it elsewhere if you want to give it another precedence
case '%':
   prec = 11;
   break;

Это оно! Мы только что расширили наш синтаксис языка для поддержки # оператор.

Мы еще не закончили, нам нужно преобразовать его обратно в JS.

Давайте сначала определим короткий visitor функция для нашего дерева, которая рекурсивно посещает все его узлы.

function visitor(tree,visit){
    for(var i in tree){
        visit(tree[i]);
        if(typeof tree[i] === "object" && tree[i] !== null){
            visitor(tree[i],visit);
        }
    }
}

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

Теперь давайте рассмотрим наш специальный новый оператор:

visitor(syntax,function(el){ // for every node in the syntax
    if(el.type === "BinaryExpression"){ // if it's a binary expression

        if(el.operator === "#"){ // with the operator #
        el.type = "CallExpression"; // it is now a call expression
        el.callee = {name:"operator_sharp",type:"Identifier"}; // for the function operator_#
        el.arguments = [el.left, el.right]; // with the left and right side as arguments
        delete el.operator; // remove BinaryExpression properties
        delete el.left;
        delete el.right;
        }
    }
});

Итак, вкратце:

var syntax = esprima.parse("5 # 5");

visitor(syntax,function(el){ // for every node in the syntax
    if(el.type === "BinaryExpression"){ // if it's a binary expression

        if(el.operator === "#"){ // with the operator #
        el.type = "CallExpression"; // it is now a call expression
        el.callee = {name:"operator_sharp",type:"Identifier"}; // for the function operator_#
        el.arguments = [el.left, el.right]; // with the left and right side as arguments
        delete el.operator; // remove BinaryExpression properties
        delete el.left;
        delete el.right;
        }
    }
});

var asJS = escodegen.generate(syntax); // produces operator_sharp(5,5);

Последнее, что нам нужно сделать, это определить саму функцию:

function operator_sharp(x,y){
    return 2*x + y;
}

И включите это выше нашего кода.

Это все, что нужно сделать! Если вы читаете до сих пор - вы заслуживаете печенье:)

Вот код на GitHub, чтобы вы могли поиграть с ним.

Как я уже сказал в комментариях к вашему вопросу, sweet.js пока не поддерживает инфиксные операторы. Вы можете раскошелиться на sweet.js и добавить его самостоятельно, или вы просто SOL.

Честно говоря, реализовывать пользовательские инфиксные операторы пока не стоит. Sweet.js - это хорошо поддерживаемый инструмент, и он единственный из известных мне, который пытается реализовать макросы в JS. Добавление пользовательских инфиксных операторов с пользовательским препроцессором, вероятно, не стоит того, что вы могли бы получить.

Тем не менее, если вы работаете над этим в одиночку для непрофессиональной работы, делайте что хотите...

Другие вопросы по тегам