Golang 编译器代码浅析
  • 0. Golang 编译器代码浅析
  • 1. golang 编译器 - 前言
    • 1.1 编译器简介
    • 1.2 Golang 编译器
    • 1.3 Go 语言版本
    • 1.4 项目设置
    • 1.5 约定
    • 1.6 写作目的
  • 2. golang 编译器 - 词法分析
    • 2.1 简介
    • 2.2 代码结构
    • 2.3 处理字符
    • 2.4 扫描Token
    • 2.5 总结
  • 3.a 语法分析理论知识
    • 3A.1 语法分析简介
    • 3A.2 文法
    • 3A.3 语法解析
    • 3A.3.1 自顶向下(Top-Down)
    • 3A.3.2 自顶向下 - 递归下降
    • 3A.3.3 自顶向下 - LL(1)文法
    • 3A.3.4 自底向上(Bottom-Up)
    • 3A.3.5 自底向上 - LR(0)项集及SLR预测表
    • 3A.3.6 自底向上 - LR(1)、LALR
    • 3A.4 语法分析工具
    • 3A.5 总结
  • 3B. golang 编译器 - 语法分析
    • 3B.1 简介
    • 3B.2 代码结构
    • 3B.3 数据结构
    • 3B.4 构造语法树
    • 3B.5 Unit Test及AST可视化
  • 4. Golang 编译器 - 类型检查
    • 4.1 简介
    • 4.2 代码结构
    • 4.3 符号解析
    • 4.4.1 数据结构 - 作用域
    • 4.4.2 数据结构 - Package
    • 4.4.3 数据结构 - Object 对象
    • 4.4.4-1 类型数据结构 - 简介
    • 4.4.4-2 类型接口
    • 4.4.4-3 基础类型
    • 4.4.4-4 内置复合类型
    • 4.4.4-5 Struct 类型
    • 4.4.4-6 Interface 类型
    • 4.4.4-7 Named 类型
    • 4.4.4-8 Tuple 类型
    • 4.4.4-9 Sum 类型
    • 4.4.4-10 Function & Method 类型
    • 4.4.4-11 泛型类型
    • 4.4.4-12 类型的等价规则
    • 4.4.4-13 类型的比较规则
    • 4.4.4-14 总结
    • 4.4.5 类型检查器
    • 4.4.6 总结
    • 4.5.1 类型检查逻辑 - 包加载器
    • 4.5.2 类型检查逻辑 - 初始化
    • 4.5.2-1 全局作用域
    • 4.5.2-2 类型检查器
    • 4.5.3 类型检查逻辑 - 流程分析
    • 4.5.3-1.1 总体流程
    • 4.5.3-1.2 类型检查准备工作
    • 4.5.3-1.3 类型检查核心逻辑
    • 4.5.3-1.3a 总体介绍
    • 4.5.3-1.3b 类型表达式的类型检查
    • 4.5.3-1.3c 求值表达式的类型检查
    • 4.5.3-1.3d 类型兼容性检查
    • 4.5.3-1.3e 处理delayed队列
    • 4.5.3-1.4 构建初始化顺序
    • 4.5.3-1.5 总结
    • 4.5.3-2 特定问题分析
    • 4.5.3-2a 对象循环依赖检查
    • 4.5.3-2b 方法与属性查找
    • 4.5.3-2c Underlying Type
    • 4.6 如何测试
    • 4.7 总结
  • 5. Golang 编译器 - IR Tree
    • 5.1 简介
    • 5.2 代码结构
    • 5.3 数据结构
    • 5.4 处理逻辑
    • 5.5 编译日志
    • 5.6 Unit Test
    • 5.7 总结
  • 6. golang 编译器 - 初始化任务
    • 6.1 简介
    • 6.2 代码结构
    • 6.3 总体逻辑
    • 6.4 赋值语句
    • 6.5 编译日志
    • 6.6 Unit Test
    • 6.7 总结
  • 7. golang 编译器 - 清除无效代码
    • 7.1 简介
    • 7.2 处理逻辑
    • 7.3 Unit Test
  • 8. golang 编译器 - Inline
    • 8.1 简介
    • 8.2 Inline的问题
    • 8.3 代码结构
    • 8.4 处理逻辑
    • 8.4.1 遍历调用链
    • 8.4.2 内联判断
    • 8.4.3 内联操作
    • 8.4.4 编译日志
    • 8.4.5 Unit Test
    • 8.4.6 总结
  • 9. golang 编译器 - 逃逸分析
    • 9.1 什么是逃逸分析
    • 9.2 Go 的逃逸分析
    • 9.3 算法思路
    • 9.4 代码结构
    • 9.5 处理逻辑
    • 9.5.1总体逻辑
    • 9.5.2 数据结构
    • 9.5.3 构建数据流有向图
    • 9.5.4 逃逸分析
    • 9.6 编译日志
    • 9.7 Unit Test
    • 9.8 总结
  • 10. golang 编译器 - 函数编译及导出
    • 10.1 简介
    • 10.2 编译函数
    • 10.2.1 SSA
    • 10.2.2 ABI
    • 10.2.3 并发控制
    • 10.3 导出对象文件
    • 10.4 总结
  • 11. Golang 编译器 - 写在最后
由 GitBook 提供支持
在本页
  • 5.4.1 相关代码
  • 5.4.2 构建入口及步骤
  • 5.4.3 Import 语句
  • 5.4.4 翻译 AST
  • 5.4.5 告别泛型

这有帮助吗?

  1. 5. Golang 编译器 - IR Tree

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 语句

处理 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 中的类型。

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 中所有的泛型函数申明。

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

上一页5.3 数据结构下一页5.5 编译日志

最后更新于3年前

这有帮助吗?

在类型检查章节,我们已经介绍过新类型检查器中有关包(package)的数据结构以及包加载器(Package Loader)的工作原理,但这部分内容当前还没有应用到编译器后续步骤中来,所以在 IR Tree 的构建阶段依然采用的是之前的加载逻辑,其总体逻辑是将一个 import 语句解析成一个 types.Pkg 对象并保存到 typecheck.Target 的 属性中。

上述的 Step 2 与 Step 3 会构建出完整的 IR Tree, Step 2 先对全局的类型申明(即通过 type 关键字申明的类型)进行转换,Step 3 对剩下的全局申明进行转换,例如常量、变量、函数申明等。所有的结果都会保存到的 Decls 属性中,这两步完成之后,该属性保存着完整的 IR Tree.

Imports
typecheck.Target
AST vs IR Tree