最后更新于
最后更新于
与构建 IR Tree 密切相关的代码存在于两个地方,一个在文件 cmd/compiler/internal/noder/irgen.go
中,是构建逻辑的入口;另一个在 cmd/compiler/internal/typecheck/target.go
中,该文件只定义了一个全局变量 Target
, 而该全局变量保存着 IR Tree 构建的全部结果。
全局变量 Target
的类型定义在 cmd/compiler/internal/ir/package.go
中,内容如下:
回忆一下:package 是 Go 编译器一次编译的最大单元!该结构用来封装被编译的包的所有信息。构建 IR Tree 的过程便是填充其中内容的过程,自此往后,编译器的操作都是基于这里面的内容来完成了。
IR Tree 构建逻辑由 irgen
驱动,其定义如下:
构建入口及绝大多数逻辑都由 irgen上的方法来完成。
构建逻辑的入口方法是 irgen.generate()
,其中包含着总体的处理步骤,我们在此处仅保留处理框架一窥大概:
注意,方法 irgen.generate()
的参数 []*noder
便是语法分析的结果:AST. 通过上述代码我们基本可以总结出 IR Tree 构建的总体逻辑:遍历 AST 并构建出 IR Tree, 并将所有的处理结果保存在全局变量 typecheck.Target
中。比较特别的是 Step 5 与 Step 6, 这两步将泛型函数的调用完全转换成了普通函数的调用,之后所有源代码中关于泛型的语义就消失了。
下面我们详细探索一下上述各步骤的处理逻辑。
处理 import 语句对应前面的 Step 1, 代码细节这里不再详细展开。
将 AST 翻译成 IR Tree 的对应上述 Step 2 与 Step 3. 这里我们先看 Step 3 入口函数的代码:
函数通过 switch...case...
对 AST 进行深度优先遍历,并且在遍历过程中根据 AST 的节点构建出对应的 IR Tree 节点。当然 AST 的节点与 IR Tree 的节点并不是一一对应的,例如对于赋值语句 names := make(chan string)
, AST 与 IR Tree 的结构如下:
上图中椭圆形代表树的节点,线段代表属性名称,矩形代表属性值。图中只显示了重要的属性值。
可见 IR Tree 与 AST 刻画了相同的程序结构,但是 IR Tree 的节点信息更加具体,例如对于 make(chan string)
, IR Tree 中对应节点的属性就比 AST 中的节点更有表现力。
构建 IR Tree 的逻辑要点可以归结如下:
翻译节点,例如上图中将 AST 中的 CallExpr
节点翻译成更加具体的 MakeExpr
节点。核心代码如下:
cmd/compile/internal/noder/stmt.go
中的 irgen.stmt()
方法用来翻译语句(Statement)
cmd/compile/internal/noder/expr.go
中的 irgen.expr()
方法用来翻译表达式(Expression)
翻译类型,将新类型检查器中所使用的 types2
类型翻译成 types
下对应的类型,例如上图中的橙色方框。核心代码如下:
cmd/compile/internal/noder/types.go
中的 irgen.typ()
方法用来将 types2
中的类型转换成 types
中的类型。
在类型检查章节中,我们讨论过泛型的实例化,泛型相当于定义了一个模版,而使用泛型的地方需要根据该模版生成实际对象。对于泛型函数而言,实例函数的生成发生在构建 IR Tree 的最后阶段。我们先了解一下泛型函数的实例化,对于代码:
这里有两个地方使用到了泛型函数 genericFun
, 如果不适用泛型的话,上述代码等价于:
前者代码量更少,对编程人员友好,后者虽然代码量更大,但编译器处理起来更简单,实际上 Go 在不支持泛型时编译的就是这样的代码,而只要明确了使用泛型函数时的类型参数,我们就可以轻松地将前者转换为后者。根据类型参数创建普通函数,便是泛型函数的实例化。
我们知道当编译器完成类型检查后,所有函数调用的类型信息都已经明确了,对泛型的使用也是如此:要么代码中显示指定了类型参数,例如上例中的 genericFun[float32](10.5, 11.8)
;要么类型检查器通过类型推导得到类型信息,例如上例中的 genericFun(5,6)
. 因此在编译阶段我们就可以完成泛型函数的实例化,事实上编译器实例化效果与上述示例代码完全一样,只有生成的函数名不同,对于上述代码,编译器使用的实例化函数名字分别是 genericFun[int]
与 genericFun[float32]
, 这种函数名称在代码中是非法的,仅在编译器内部使用。
泛型函数的实例化会改变 IR Tree 的结构及内容,一是创建出了新的全局函数申明,需要将其添加到 IR Tree 中,二是函数调用的节点需要修改,三是再完成实例化后,泛型函数的申明就不再需要了,即我们可以从 IR Tree 中将其删除。前两点在 Step 5 中通过函数 irgen.stencil()
完成,所有的相关逻辑都定义在文件 cmd/compile/internal/stencil.go
中。而最后一步 Step 6 则删除了 IR Tree 中所有的泛型函数申明。
自此,我们的代码就告别泛型,又回归最传统的形式了。
在类型检查章节,我们已经介绍过新类型检查器中有关包(package)的数据结构以及包加载器(Package Loader)的工作原理,但这部分内容当前还没有应用到编译器后续步骤中来,所以在 IR Tree 的构建阶段依然采用的是之前的加载逻辑,其总体逻辑是将一个 import 语句解析成一个 types.Pkg
对象并保存到 typecheck.Target
的 属性中。
上述的 Step 2 与 Step 3 会构建出完整的 IR Tree, Step 2 先对全局的类型申明(即通过 type
关键字申明的类型)进行转换,Step 3 对剩下的全局申明进行转换,例如常量、变量、函数申明等。所有的结果都会保存到的 Decls 属性中,这两步完成之后,该属性保存着完整的 IR Tree.