5.4 处理逻辑

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.TargetImports 属性中。

处理 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 的逻辑要点可以归结如下:

  1. 翻译节点,例如上图中将 AST 中的 CallExpr 节点翻译成更加具体的 MakeExpr 节点。核心代码如下:

    • cmd/compile/internal/noder/stmt.go 中的 irgen.stmt() 方法用来翻译语句(Statement)

    • cmd/compile/internal/noder/expr.go 中的 irgen.expr() 方法用来翻译表达式(Expression)

  2. 翻译类型,将新类型检查器中所使用的 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 中所有的泛型函数申明。

自此,我们的代码就告别泛型,又回归最传统的形式了。

最后更新于