是的,这是可能的,甚至并不是很难 :)
我们需要讨论一些问题:
- 什么是语法和语义。
- 编程语言是如何解析的?什么是语法树?
- 扩展语言语法。
- 扩展语言语义。
- 我如何向JavaScript语言添加运算符。
如果你懒得看解释只想看到实际操作-我在GitHub上放了完整的代码
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,因为我们将给它不理解的语法。
我们将添加一个 #
运算符,它执行 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){
if(el.type === "BinaryExpression"){
if(el.operator === "#"){
el.type = "CallExpression";
el.callee = {name:"operator_sharp",type:"Identifier"};
el.arguments = [el.left, el.right];
delete el.operator;
delete el.left;
delete el.right;
}
}
});
简而言之:
var syntax = esprima.parse("5 # 5");
visitor(syntax,function(el){
if(el.type === "BinaryExpression"){
if(el.operator === "#"){
el.type = "CallExpression";
el.callee = {name:"operator_sharp",type:"Identifier"};
el.arguments = [el.left, el.right];
delete el.operator;
delete el.left;
delete el.right;
}
}
});
var asJS = escodegen.generate(syntax);
我们需要做的最后一件事是定义函数本身:
function operator_sharp(x,y){
return 2*x + y;
}
将其包含在我们代码之上。
就是这样!如果你一直读到这里 - 你应该得到一个饼干 :)
这是GitHub上的代码,让您可以尝试它。