Как бы я расширил язык 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 ответа
Да, это возможно и даже не очень сложно:)
Нам нужно обсудить несколько вещей:
- Что такое синтаксис и семантика.
- Как анализируются языки программирования? Что такое синтаксическое дерево?
- Расширение синтаксиса языка.
- Расширение языковой семантики.
- Как добавить оператор в язык 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. Добавление пользовательских инфиксных операторов с пользовательским препроцессором, вероятно, не стоит того, что вы могли бы получить.
Тем не менее, если вы работаете над этим в одиночку для непрофессиональной работы, делайте что хотите...