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

29

问题“在JavaScript中是否可能创建自定义运算符?”的答案是尚未,但@Benjamin 提出可以使用第三方工具添加新的运算符:

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

我将使用与之前问题相同的示例:

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

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

运行以下代码后:

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

console.log(z);

输出结果将包含

8

(因为82 + 2 * 3)


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


1
这个问题和你之前的那个问题有什么不同? - Oliver Charlesworth
4
这是更详细的说明。在之前的问题中,我问过是否可能,现在我想知道如何添加一个新操作符来扩展语言。 - Ionică Bizău
2
@MikeW 不,他们没有。我不认为这是一个重复的问题。真正的解决方案与之前的问题没有什么关系。但对于这个问题来说,它是相关的。最接近的答案是Benjamin的答案,他说可以使用第三方工具实现。 - Ionică Bizău
1
投票关闭。如果您想获得更针对使用sweet.js的答案,您应该以那种方式提问。 - Scott Sauyet
1
Sweet.js目前还不支持中缀运算符:https://github.com/mozilla/sweet.js/issues/34 - Florian Margaine
显示剩余6条评论
2个回答

57

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


我们需要讨论一些问题:

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

如果你懒得看解释只想看到实际操作-我在GitHub上放了完整的代码

1. 什么是语法和语义?

总的来说-一个语言由两部分组成。

  • 语法-这些是语言中的符号,例如一元运算符如++,还有像FunctionExpression这样代表“内联”函数的Expression。语法只表示所使用的符号,而不表示它们的含义。简而言之语法只是字母和符号的图纸-它没有固有的含义。

  • 语义将这些符号与含义联系起来。语义是说++的含义是“加1”,实际上这里是确切的定义。它将含义与语法绑定,如果没有它,语法只是具有顺序的符号列表。

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

在某个时刻,当JavaScript或任何其他编程语言中的某些东西执行您的代码时-它需要理解该代码。其中一部分称为词法分析(或标记化),意味着将类似以下代码的内容分解为:

function foo(){ return 5;}

将其分解为有意义的部分 - 这意味着这里有一个function关键字,后面跟着一个标识符,一个空的参数列表,然后是一个打开块{,其中包含一个带有文字5的返回关键字,然后是一个分号,然后是一个结束块}

这部分完全属于语法,它只是将代码分成像function,foo,(,),{,return,5,;,}这样的部分。它仍然没有理解代码的含义。

然后 - 构建语法树。语法树更了解语法,但仍然完全是句法上的。例如,语法树会看到以下标记:

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,它执行另一个方向,从 esprima 生成语法树生成 JS 代码。

如果你仔细留意,你会知道我们不能直接使用 esprima,因为我们将给它不理解的语法。

我们将添加一个 # 运算符,它执行 x # y === 2x + y。我们将赋予它乘法的优先级(因为运算符具有运算符优先级)。

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

对于表达式(expressions),我们需要添加 #,以便识别它。之后,它看起来如下:

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

这只是遍历 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;
        }
    }
});

简而言之:

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上的代码,让您可以尝试它。


9
比简单的“不行”回答好多了。 :-) - Ionică Bizău
我会倾向于在答案的后半部分使用 而不是 #,以保持一致性... - Donal Fellows
@BenjaminGruenbaum 又有东西给你了! :-) - Ionică Bizău
1
你使用的 esprima 版本是什么?能否提供一个 v4.0 的示例?我的 esprima 没有 FnExprTokens,而这个答案似乎已经过时了五年。 - kepe
1
整个堆栈变得非常不同,今天我会将其作为Babel转换(或TypeScript转换)来处理。这里有一个使用管道运算符的示例:https://github.com/babel/babel/tree/master/packages/babel-plugin-proposal-pipeline-operator/src。本答案中的代码部分已经过时。 - Benjamin Gruenbaum
显示剩余6条评论

3
正如我在你的问题评论中所说,sweet.js目前不支持中缀运算符。你可以自由地fork sweet.js并自己添加它,或者你只能接受现实。
老实说,目前还没有必要实现自定义中缀运算符。Sweet.js是一个得到很好支持的工具,而且它是我所知道的唯一试图在JS中实现宏的工具。使用自定义预处理器添加自定义中缀运算符可能得不偿失。
话虽如此,如果你是为了非专业工作而独自进行此项工作,那就随心所欲吧...
编辑

sweet.js现在已经支持中缀运算符


2
只想指出,sweet.js 的主页上介绍了中缀运算符的支持,请查看示例。此外,在 我的 candyshop 存储库 上还有 if、range 和 unless 宏。 - locks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接