当前包中的词法分析器 scanner 也只是为上层提供了 next 方法,词法解析的过程都是惰性的,只有在上层的解析器需要时才会调用 next 获取最新的 Token 。
语法分析
上一步生成的 Token 序列,需要经过进一步处理,生成一棵以 表达式为结点的 语法树 。
比如最开始的那个例子, slice[i]=i*(2+6),得到的一棵语法树如下:
文章插图
整个语句被看作是一个赋值表达式,左子树是一个数组表达式,右子树是一个乘法表达式;数组表达式由 2 个符号表达式组成;乘号表达式则是由一个符号表达式和一个加号表达式组成;加号表达式则是由两个数字组成 。符号和数字是最小的表达式,它们不能再被分解,通常作为树的叶子节点 。
语法分析的过程可以检测一些形式上的错误,例如:括号是否缺少一半, + 号表达式缺少一个操作数等 。
语法分析是根据某种特定的形式文法(Grammar)对 Token 序列构成的输入文本进行分析并确定其语法结构的一种过程 。
语义分析
语法分析完成后,我们并不知道语句的具体意义是什么 。像上面的 * 号的两棵子树如果是两个指针,这是不合法的,但语法分析检测不出来,语义分析就是干这个事 。
编译期所能检查的是静态语义,可以认为这是在“代码”阶段,包括变量类型的匹配、转换等 。例如,将一个浮点值赋给一个指针变量的时候,明显的类型不匹配,就会报编译错误 。而对于运行期间才会出现的错误:不小心除了一个 0 ,语义分析是没办法检测的 。
语义分析阶段完成之后,会在每个节点上标注上类型:
文章插图
Go 语言编译器在这一阶段检查常量、类型、函数声明以及变量赋值语句的类型,然后检查哈希中键的类型 。实现类型检查的函数通常都是几千行的巨型 switch/case 语句 。
类型检查是 Go 语言编译的第二个阶段,在词法和语法分析之后我们得到了每个文件对应的抽象语法树,随后的类型检查会遍历抽象语法树中的节点,对每个节点的类型进行检验,找出其中存在的语法错误 。
在这个过程中也可能会对抽象语法树进行改写,这不仅能够去除一些不会被执行的代码对编译进行优化提高执行效率,而且也会修改 make、new 等关键字对应节点的操作类型 。
例如比较常用的 make 关键字,用它可以创建各种类型,如 slice,map,channel 等等 。到这一步的时候,对于 make 关键字,也就是 OMAKE 节点,会先检查它的参数类型,根据类型的不同,进入相应的分支 。如果参数类型是 slice,就会进入 TSLICE case 分支,检查 len 和 cap 是否满足要求,如 len <= cap 。最后节点类型会从 OMAKE 改成 OMAKESLICE 。
中间代码生成
我们知道,编译过程一般可以分为前端和后端,前端生成和平台无关的中间代码,后端会针对不同的平台,生成不同的机器码 。
前面词法分析、语法分析、语义分析等都属于编译器前端,之后的阶段属于编译器后端 。
编译过程有很多优化的环节,在这个环节是指源代码级别的优化 。它将语法树转换成中间代码,它是语法树的顺序表示 。
中间代码一般和目标机器以及运行时环境无关,它有几种常见的形式:三地址码、P-代码 。例如,最基本的 三地址码是这样的:
x = y op z
表示变量 y 和 变量 z 进行 op 操作后,赋值给 x 。op 可以是数学运算,例如加减乘除 。前面我们举的例子可以写成如下的形式:
t1 = 2 + 6 t2 = i * t1slice[i] = t2
这里 2 + 6 是可以直接计算出来的,这样就把 t1 这个临时变量“优化”掉了,而且 t1 变量可以重复利用,因此 t2 也可以“优化”掉 。优化之后:t1 = i * 8 slice[i] = t1
Go 语言的中间代码表示形式为 SSA(Static Single-Assignment,静态单赋值),之所以称之为单赋值,是因为每个名字在 SSA 中仅被赋值一次 。这一阶段会根据 CPU 的架构设置相应的用于生成中间代码的变量,例如编译器使用的指针和寄存器的大小、可用寄存器列表等 。中间代码生成和机器码生成这两部分会共享相同的设置 。
在生成中间代码之前,会对抽象语法树中节点的一些元素进行替换 。这里引用《面向信仰编程》编译原理相关博客里的一张图:
推荐阅读
- 详解3种区别Linux服务器是物理机或者虚拟机的方法
- 详解Docker可视化管理工具shipyard--部署教程及功能展示
- 绝对经典,看了必会 linux中部署mysql主从同步示例详解
- 详解Oracle数据库物理设计--表和索引设计建议
- 详解微服务技术中进程间通信
- ”什么是内网穿透“详解
- 局域网中NAT具体工作过程详解
- MySQL执行计划命令EXPLAIN详解
- 静态路由详解
- 计算机启动过程详解