5.4.1 相关代码
与构建 IR Tree 密切相关的代码存在于两个地方,一个在文件 cmd/compiler/internal/noder/irgen.go
中,是构建逻辑的入口;另一个在 cmd/compiler/internal/typecheck/target.go
中,该文件只定义了一个全局变量 Target
, 而该全局变量保存着 IR Tree 构建的全部结果。
全局变量 Target
的类型定义在 cmd/compiler/internal/ir/package.go
中,内容如下:
复制 type Package struct {
Imports [] * types . Pkg // <<Imports>>
// Init functions, listed in source order.
Inits [] * Func
// Top-level declarations.
Decls [] Node
// Extern (package global) declarations.
Externs [] Node
// Assembly function declarations.
Asms [] * Name
// Cgo directives.
CgoPragmas [][] string
// Variables with //go:embed lines.
Embeds [] * Name
// Exported (or re-exported) symbols.
Exports [] * Name
// Map from function names of stencils to already-created stencils.
Stencils map [ * types . Sym ] * Func
}
回忆一下:package 是 Go 编译器一次编译的最大单元!该结构用来封装被编译的包的所有信息。构建 IR Tree 的过程便是填充其中内容的过程,自此往后,编译器的操作都是基于这里面的内容来完成了。
IR Tree 构建逻辑由 irgen
驱动,其定义如下:
复制 // file: cmd/compiler/internal/noder/irgen.go
type irgen struct {
target * ir . Package // 当前对应全局变量 typecheck.Target
self * types2 . Package
info * types2 . Info // types2 类型检查的结果
posMap
objs map [ types2 . Object ] * ir . Name
typs map [ types2 . Type ] * types . Type
marker dwarfgen . ScopeMarker
// Fully-instantiated generic types whose methods should be instantiated
instTypeList [] * types . Type
}
构建入口及绝大多数逻辑都由 irgen上的方法来完成。
5.4.2 构建入口及步骤
构建逻辑的入口方法是 irgen.generate()
,其中包含着总体的处理步骤,我们在此处仅保留处理框架一窥大概:
复制 func (g * irgen ) generate (noders [] * noder ) {
declLists := make ([][] syntax . Decl , len (noders))
Outer:
// Step 1: 处理 import 语句,将对应的包加载并解析成 types.Pkg 对象,本质上是根据 import 语句实例化 typecheck.Target.Imports 字段
for i, p := range noders {
g.pragmaFlags(p.file.Pragma, ir.GoBuildPragma)
for j, decl := range p.file.DeclList {
switch decl := decl.( type ) {
case * syntax . ImportDecl :
// 处理 Import 语句
g.importDecl(p, decl)
default :
// 非 Import 语句保存起来留着后续步骤处理
declLists[i] = p.file.DeclList[j:]
continue Outer // no more ImportDecls
}
}
}
// Step 2: 处理全局的类型申明,对于每个全局类型申明,创建一个对等的 ir.Decl 对象并将其存入 typecheck.Target.Decls 中
// 1. 创建 Name 对象
// 2. 将 types2.Type 转换为对应的 types.Type
// 3. 处理该类型的方法,创建对应的 Field 对象
for _, declList := range declLists {
for _, decl := range declList {
switch decl := decl.( type ) {
case * syntax . TypeDecl :
g.typeDecl(( * ir . Nodes )( & g.target.Decls), decl)
}
}
}
// Step 3: 处理其他类型的申明,即全局常量申明、全局变量申明、函数申明。为每个申明创建出对应的 ir.Node 并保存在 typecheck.Target.Decls 中
for _, declList := range declLists {
// shizhz -
g.target.Decls = append (g.target.Decls, g.decls(declList) ... )
}
// Step 4: 注册所有的 builtin 函数到 types.LocalPkg 中
typecheck.DeclareUniverse()
// Step 5: 根据泛型函数使用情况创建函数,该过程叫着泛型函数的实例化,后续会专门介绍
g.stencil()
// Step 6: 去掉所有的泛型函数申明,详见后续关于第5步的介绍
j := 0
for i, decl := range g.target.Decls {
if decl.Op() != ir.ODCLFUNC || ! decl.Type().HasTParam() {
g.target.Decls[j] = g.target.Decls[i]
j ++
}
}
g.target.Decls = g.target.Decls[:j]
}
注意,方法 irgen.generate()
的参数 []*noder
便是语法分析的结果:AST. 通过上述代码我们基本可以总结出 IR Tree 构建的总体逻辑:遍历 AST 并构建出 IR Tree, 并将所有的处理结果保存在全局变量 typecheck.Target
中。比较特别的是 Step 5 与 Step 6, 这两步将泛型函数的调用完全转换成了普通函数的调用,之后所有源代码中关于泛型的语义就消失了。
下面我们详细探索一下上述各步骤的处理逻辑。
5.4.3 Import 语句
在类型检查章节,我们已经介绍过新类型检查器中有关包(package)的数据结构以及包加载器(Package Loader)的工作原理,但这部分内容当前还没有应用到编译器后续步骤中来,所以在 IR Tree 的构建阶段依然采用的是之前的加载逻辑,其总体逻辑是将一个 import 语句解析成一个 types.Pkg
对象并保存到 typecheck.Target
的 Imports 属性中。
处理 import 语句对应前面的 Step 1, 代码细节这里不再详细展开。
5.4.4 翻译 AST
将 AST 翻译成 IR Tree 的对应上述 Step 2 与 Step 3. 这里我们先看 Step 3 入口函数的代码:
复制 func (g * irgen ) decls (decls [] syntax . Decl ) [] ir . Node {
var res ir . Nodes
for _, decl := range decls {
switch decl := decl.( type ) {
case * syntax . ConstDecl :
g.constDecl( & res, decl)
case * syntax . FuncDecl :
g.funcDecl( & res, decl)
case * syntax . TypeDecl :
if ir.CurFunc == nil {
continue // already handled in irgen.generate
}
g.typeDecl( & res, decl)
case * syntax . VarDecl :
g.varDecl( & res, decl)
default :
g.unhandled( "declaration" , decl)
}
}
return res
}
函数通过 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
中的类型。
上述的 Step 2 与 Step 3 会构建出完整的 IR Tree, Step 2 先对全局的类型申明(即通过 type
关键字申明的类型)进行转换,Step 3 对剩下的全局申明进行转换,例如常量、变量、函数申明等。所有的结果都会保存到typecheck.Target 的 Decls 属性中,这两步完成之后,该属性保存着完整的 IR Tree.
5.4.5 告别泛型
在类型检查章节中,我们讨论过泛型的实例化,泛型相当于定义了一个模版,而使用泛型的地方需要根据该模版生成实际对象。对于泛型函数而言,实例函数的生成发生在构建 IR Tree 的最后阶段。我们先了解一下泛型函数的实例化,对于代码:
复制 package main
import "fmt"
func genericFun [T any ](i, j T ) {
fmt.Printf( "i: %v , j: %v \n" ,i, j)
}
func main () {
genericFun( 5 , 6 )
genericFun[ float32 ]( 10.5 , 11.8 )
}
这里有两个地方使用到了泛型函数 genericFun
, 如果不适用泛型的话,上述代码等价于:
复制 package main
import "fmt"
func genericFunInt (i, j int ) {
fmt.Printf( "i: %v , j: %v \n" , i, j)
}
func genericFunFloat32 (i, j float32 ) {
fmt.Printf( "i: %v , j: %v \n" , i, j)
}
func main () {
genericFunInt( 5 , 6 )
genericFunFloat32( 10.5 , 11.8 )
}
前者代码量更少,对编程人员友好,后者虽然代码量更大,但编译器处理起来更简单,实际上 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 中所有的泛型函数申明。
自此,我们的代码就告别泛型,又回归最传统的形式了。