我如何扩展JavaScript语言以支持新的运算符?

Ion*_*zău 22 javascript operators language-extension node.js

问题的答案是否可以在JavaScript中创建自定义运算符?没有,但@Benjamin建议可以使用第三方工具添加新的运算符:

虽然需要额外的编译步骤,但可以使用像sweet.js这样的第三方工具来添加自定义运算符.

我会采用相同的例子,就像上一个问题一样:

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

对于任何两个实数xy:x∘yx + 2y,也是实数.如何在我的扩展JavaScript语言中添加此运算符?

运行以下代码后:

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

console.log(z);
Run Code Online (Sandbox Code Playgroud)

输出将包含

8
Run Code Online (Sandbox Code Playgroud)

(因为82 + 2 * 3)


我如何扩展JavaScript语言以支持新的运算符?

Ben*_*aum 43

是的,这是可能的,甚至不是很难:)


我们需要讨论一些事情:

  1. 什么是语法和语义.
  2. 如何解析编程语言?什么是语法树?
  3. 扩展语言语法.
  4. 扩展语言语义.
  5. 如何向JavaScript语言添加运算符.

如果你很懒,只是想看看它的实际效果 - 我把工作代码放在GitHub上

1.什么是语法和语义?

一般来说 - 一种语言由两件事组成.

  • 语法 -这是在像元运算符一样的语言符号++,以及Expression像是一个FunctionExpression表示"内联"的功能.语法仅代表使用的符号,而不代表其含义.简而言之,语法只是字母和符号的图形 - 它没有固有的含义.

  • 语义与这些符号有关.语义就是所谓的++"递增一",实际上这就是确切的定义.它与我们的语法有意义,没有它,语法只是带有顺序的符号列表.

2.如何解析编程语言?什么是语法树?

在某些时候,当某些东西用JavaScript或任何其他编程语言执行你的代码时 - 它需要理解那些代码.这个名为lexing的一部分(或标记化,我们不会在这里进行细微差别)意味着分解如下代码:

function foo(){ return 5;}
Run Code Online (Sandbox Code Playgroud)

进入其有意义的部分 - 也就是说function这里有一个关键字,后跟一个标识符,一个空参数列表,然后是一个{包含带有文字的return关键字的块开头5,然后是一个分号,然后是一个结束块}.

这部分完全在语法中,它所做的就是将它分解为类似的部分function,foo,(,),{,return,5,;,}.它仍然不了解代码.

在那之后 - 一个Syntax Tree建成.语法树更了解语法,但仍然完全是语法.例如,语法树会看到以下标记:

function foo(){ return 5;}
Run Code Online (Sandbox Code Playgroud)

并弄清楚"嘿!这里有一个函数声明!".

它被称为树,因为它只是 - 树允许嵌套.

例如,上面的代码可以产生如下内容:

                                        Program
                                  FunctionDeclaration (identifier = 'foo')
                                     BlockStatement
                                     ReturnStatement
                                     Literal (5)
Run Code Online (Sandbox Code Playgroud)

这很简单,只是为了告诉你它并不总是如此线性,让我们检查一下5 +5:

                                        Program
                                  ExpressionStatement
                               BinaryExpression (operator +)
                            Literal (5)       Literal(5)   // notice the split her
Run Code Online (Sandbox Code Playgroud)

可能发生这种分裂.

基本上,语法树允许我们表达语法.

这是x ? y失败的地方 - 它看到?并且不理解语法.

3.扩展语言语法.

这只需要一个解析语法的项目.我们在这里要做的是阅读"我们的"语言的语法,它与JavaScript不同(并且不符合规范),并用JavaScript语法可以替换我们的运算符.

我们要做的不是 JavaScript.它不遵循JavaScript规范和标准投诉JS解析器将抛出异常.

4.扩展语言语义

我们总是这样做:)我们在这里所做的只是定义一个在调用运算符时调用的函数.

5.如何向JavaScript语言添加运算符.

让我先说一下这个前缀后我们不会在这里添加一个运算符,而是 - 我们正在定义我们自己的语言 - 让我们称它为"CakeLanguage"或其他东西并添加运算符吧.这是因为?它不是JS语法的一部分,并且JS语法不允许像其他语言那样的任意运算符.

我们将使用两个开源项目:

  • esprima,它接受JS代码并为其生成语法树.
  • escodegen执行另一个方向,从语法树esprima spits生成JS代码.

你密切注意你知道我们不能直接使用esprima因为我们会给它不懂的语法.

我们将添加一个有趣的#运算符x # y === 2x + y.我们将赋予它多重性的优先级(因为运算符具有运算符优先级).

因此,在您获得Esprima.js副本后 - 我们需要更改以下内容:

FnExprTokens- 这是我们需要添加的表达式,#以便识别它.之后,它看起来像这样:

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

为了scanPunctuator我们将添加它和它的字符代码作为可能的情况:case 0x23: // #

然后进行测试,看起来像:

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

代替:

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

然后binaryPrecedence让它给它与多重性相同的优先级:

case '*':
case '/':
case '#': // put it elsewhere if you want to give it another precedence
case '%':
   prec = 11;
   break;
Run Code Online (Sandbox Code Playgroud)

而已!我们刚刚扩展了语言语法以支持#运算符.

我们尚未完成,我们需要将其转换回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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这只是通过Esprima生成的树并访问它.我们传递一个函数,它在每个节点上运行.

现在,让我们来看待我们的特殊新运营商:

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;
        }
    }
});
Run Code Online (Sandbox Code Playgroud)

简而言之:

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);
Run Code Online (Sandbox Code Playgroud)

我们需要做的最后一件事是定义函数本身:

function operator_sharp(x,y){
    return 2*x + y;
}
Run Code Online (Sandbox Code Playgroud)

并将其包含在我们的代码之上.

这里的所有都是它的!如果你读到目前为止 - 你应该得到一个cookie :)

这是GitHub上代码,所以你可以玩它.

  • 比简单的*好多了,不可能*答案.:-) (5认同)