does not name a type是什么意思_以JS为例, 10分钟了解代码编译时都做了什么

本文授权转载自原作者黄瑞敏, 未经许可请勿转载

编译,其实就是把源代码变成机器能理解的目标代码(如汇编语言)的过程。

编译的主要步骤主要如下:

  1. 词法分析:把输入的字符串流变成 token
  2. 语法分析:把 token 变成抽象语法树 AST
  3. 语义分析:遍历AST树,根据已有的语义规则做语义理解和语义检查。
  4. 生成目标语言(如汇编语言)。

本篇文章分析以 javascript 为程序语言的基本编译过程及相关原理。

词法分析

简单理解,就是将程序(字符串形式)进行解析:

例如对于

10 + 2 * 30

我们已经定义好这些词法:

  • TokenNumber: 1 2 3 4 5 6 7 8 9 0 的组合
  • Operator: + 、-、 *、 / 之一
  • Whitespace:
  • LineTerminator:

空白和换行没有意义,因此不加入后续语义分析。

怎么将程序字符解析出我们需要的一个一个的 token ,有两种方法,一种是状态机,一种正则;

这里用状态机demo作为例子解析 10 + 2 * 30

let token = [];
let result = [];
const inSingleChar = char => {  // 分析单个字符
    if(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'].indexOf(char) > -1) {
        token.push(char);
        return inNumber;
    }
    if(['+', '-', '*', '/'].indexOf(char) > -1) {
        emmitToken(char, char);
        return inSingleChar
    }
    if(char === ' ') {
        return inSingleChar;
    }
    if(char === '\r'
        || char === '\n'
    ) {
        return inSingleChar;
    }
}
const inNumber = char => {
    if(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'].indexOf(char) > -1) {
        token.push(char);
        return inNumber;
    } else {
        emmitToken("Number", token.join(""));
        token = [];
        return inSingleChar(char);
    }
}

function emmitToken(type, value) {
    console.log(value);
    result.push({
        type,
        value: value
    });
}

接下来运行上述代码:

let input = "24 + 2 * 100";

let state = inSingleChar;

for(var c of input.split(''))
    state = state(c);

state(Symbol('EOF')); // EOF为文件结束符

result.push({
    type: 'EOF'
})

运行之后,结果如下:

24
+
2
*
100

得到的result,即解析得到的 token如下:

[{
    type: 'Number',
    value: '24'
},
{
    type: '+',
    value: '+'
},
{
    type: 'Number',
    value: '2'
},
{
    type: '*',
    value: '*'
},
{
    type: 'Number',
    value: '100'
},
{
    type: 'EOF'
}]

这样我们就对这段表达式程序进行了正确的词法“分割”,这个函数虽然实现得简陋,但是实际上就是一个有限状态机[1],如图:

06026e646531ab62a7a1bee1b7da0e16.png

当然,真正的分析词法的状态机要复杂很多,因为不只是有数字和上述几个特定的字符。

语法分析

词法分析得到 token 之后就是语法分析; 将其转换成 AST语法树 ,那么什么是 AST语法树

Abstract Syntax Tree, 是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有三个分支的节点来表示。

是不是很抽象,咋一看确实有点难以理解,这里介绍一个工具,“AST螺丝刀” recast[2]

recast 能将程序转化成AST语法树;

首先安装 npm i recast

接着,可以在vs code中调试这段代码:

const recast = require("recast");
const code = `10 + 2 * 30`;
const ast = recast.parse(code);
const add  = ast.program.body[0];

可以看见ast对象中有token这个数组记录了组成该表达式的字符:

13c56aa41b2b5b578551053ffec95af6.png

再看 ast.program.body[0] 这个对象:

866b10059babd8f49cf20f65bc5cd25f.png

其实就是AST语法树

86faa0fc7a9809a2f7d1a561b3585bbf.png

这里只是一个简单的表达式,如果是一个完整的函数会是怎么样的呢?

可以再看这个例子:

const recast = require("recast");
const code =
  `
  function add(a, b) {
    var sum = 10 + 2 * 30;
    return sum;
  }
  `;
const ast = recast.parse(code);
const add  = ast.program.body[0];

调试出来的 ast.program.body[0] 如图:

39486368bfd325203958aea017ca4b4a.png

也就是包含这些属性:

FunctionDeclaration{
    type: 'FunctionDeclaration',
    params:
   [ Identifier { type: 'Identifier', name: 'a', loc: [Object] },
     Identifier { type: 'Identifier', name: 'b', loc: [Object] } ],
     body: {
         body: [VariableDeclaration, ReturnStatement],
         type: 'BlockStatement'
     }

}

可视化为树形结构即为:

ec8d492ae229f2c34a2a69b7db195814.png

注意:组成这棵 AST语法树 的节点“部件”可以在:AST对象文档[3]中找到。

那么实际中怎么将一个一个 Token 转换成上述 AST语法树 呢?这里涉及到的算法一般有两种,推荐自行查阅 LL算法[4]或LR算法[5]

语义分析

生成 AST语法树 后,程序的语法结构就很清晰了,但是机器如何理解这棵“树”的呢?实际上,语言的设计者在定义类似“a+3”中加号这个操作符的时候,是给它规定了一些语义的,就是要把加号两边的数字相加。例如在 ECMAScript(也就是 JavaScript)标准 2020 版 加法操作的语义规则[6] 中,对于如何计算左节点、右节点的值,如何进行类型转换等有这样的规定:

6c5ed9ccb7b451e4f8aeb03cbfa466bf.png

同时语义言分析也会进行标注,做上下文相关的分析。并对不符合规范的地方进行报错。

生成目标代码

之后,编译器这时可以说可以直接生成目标代码了,因为编译器已经完全理解了程序的含义,并把它表示成了带有语义信息的 AST、符号表等数据结构。其实实际编译过程还会再生成中间代码(处于源代码和目标代码之间的一种表示形式)及进行优化。

例如:跳过方法如 person.getName() 的调用,直接根据对象的地址计算出 name 属性的地址,然后直接从内存取值。(该优化法称为内联)

中间代码可以使得优化更加简易,而优化可以使目标代码运行效率更高。

总结

理解了代码运行前都做了什么,能更宏观地了解一门语言,例如,在列javascript知识框架时,可以这么列出它的框架:

468fbf7f80cb757083cc087b45d7a6eb.png

然而本文只是简单介绍了以javascript为例子的简单的代码编译机制,当然其中涉及到的每一个知识点都并不简单,因此需要由浅入深,逐步探索。

参考资料

[1]

有限状态机: https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA

[2]

recast: https://github.com/benjamn/recast

[3]

AST对象文档: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API#Node_objects

[4]

LL算法: https://zh.wikipedia.org/wiki/LL%E5%89%96%E6%9E%90%E5%99%A8

[5]

LR算法: https://zh.wikipedia.org/wiki/LR%E5%89%96%E6%9E%90%E5%99%A8

[6]

加法操作的语义规则: https://tc39.es/ecma262/2020/#sec-additive-operators


版权声明:本文为weixin_39534759原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。