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

时间:2013-12-24 14:07:14

标签: javascript node.js operators language-extension

Is it possible to create custom operators in JavaScript? 这个问题的答案是 尚未 ,但@Benjamin suggested表示可以使用第三方工具添加新运算符

  

虽然需要额外的编译步骤,但可以使用像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

(因为82 + 2 * 3


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

2 个答案:

答案 0 :(得分:40)

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


我们需要讨论一些事情:

  1. 什么是语法和语义。
  2. 如何解析编程语言?什么是语法树?
  3. 扩展语言语法。
  4. 扩展语言语义。
  5. 如何向JavaScript语言添加运算符。
  6. 如果你很懒,只是想看到它的实际效果 - I put the working code on GitHub

    1。什么是语法和语义?

    非常普遍 - 一种语言由两件事组成。

    • 语法 - 这些是语言中的符号,如++等一元运算符,以及代表FunctionExpressionExpression "内联"功能。语法仅表示使用的符号,不表示的含义。简而言之,语法只是字母和符号的图纸 - 它没有固有的含义。

    • 语义与这些符号有关。语义就是说++意味着"增加一个",实际上这里是the exact defintion。它与我们的语法有意义,没有它,语法只是一个带有顺序的符号列表。

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

    在某些时候,当某些东西用JavaScript或任何其他编程语言执行代码时 - 它需要理解该代码。其中一部分称为 lexing (或标记化,让我们在这里不会产生微妙的差异)意味着分解代码如下:

    function foo(){ return 5;}
    

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

    这部分语法中的完全,它所做的只是将其分解为}等部分。它仍然不理解代码。

    之后 - 构建function,foo,(,),{,return,5,;,}。语法树更了解语法,但仍然完全是语法。例如,语法树会看到以下标记:

    Syntax Tree

    弄清楚"嘿!这里有function declaration!"。

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

    例如,上面的代码可以产生类似:

    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语言添加运算符。

    首先我要说的是,在这个前缀之后,我们

    称之为" CakeLanguage"或其他东西,并添加操作员。这是因为不是JS语法的一部分,而且JS语法不允许像some other languages这样的任意运算符。

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

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

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

    我们会添加运算符#以获得乐趣。我们将它赋予多重性的优先级(因为运算符具有运算符优先级)。

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

    x # y === 2x + y - 即表达式,我们需要添加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

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

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

    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);
    

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

    这就是它的全部!如果你到目前为止阅读 - 你应该得到一个cookie:)

    以下是code on GitHub,因此您可以使用它。

答案 1 :(得分:3)

正如我在你的问题的评论中所说,sweet.js doesn't support infix operators。你可以自由地分叉sweet.js并自己添加,或者你只是SOL。

老实说,实现自定义中缀运算符并不值得。 Sweet.js是一个很好的支持工具,它是我所知道的唯一尝试在JS中实现宏的工具。使用自定义预处理器添加自定义中缀运算符可能不值得您获得。

那就是说,如果你正在为非职业工作单独工作,那就做你想做的事......