编译器基础知识
引用2020 Gopher China 史斌大佬的一张图
简化一下,当前文章只关注语法分析和类型检查即可
注:当前文章go版本 master 690a8c3fb1 ,应该是go1.17左右的版本。
开始探究iota
参考杨文大佬的文章带你开始探究 Go iota
Within a constant declaration, the predeclared identifier
iota
represents successive untyped integer constants. Its value is the index of the respective ConstSpec in that constant declaration, starting at zero. It can be used to construct a set of related constants:
在常量声明中,iota
表示连续的无类型整数常量,它的值是从零开始的索引,可以用来构造一组相关的常量:
先看一个简单的例子
package main
import "fmt"
const (
one = iota
two = iota
three = iota
)
func main() {
fmt.Println(one, two, three)
}
// Output:
// 0 1 2
先从这个例子入手,后续再分析复杂的场景
Note:例子中的one、two、three实际值为0、1、2,后续流程分析完才发现这个问题,不修改例子了。
先打印AST看一下,做一下大概的了解。
package main
import (
"go/ast"
"go/parser"
"go/token"
"log"
"path/filepath"
)
func main() {
fset := token.NewFileSet()
// 这里取绝对路径,方便打印出来的语法树可以转跳到编辑器
path, _ := filepath.Abs("XXX/main.go")
f, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
if err != nil {
log.Println(err)
return
}
// 打印语法树
ast.Print(fset, f)
}
0 *ast.File {
1 . Package: XXX/main.go:1:1
2 . Name: *ast.Ident {
3 . . NamePos: XXX/main.go:1:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 3) { // 共有3个Top Level Decl
7 . . 0: *ast.GenDecl { // import "fmt"
8 . . . TokPos: XXX/main.go:3:1
9 . . . Tok: import
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: XXX/main.go:3:8
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"fmt\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: -
22 . . }
23 . . 1: *ast.GenDecl { // const ( XXX )
24 . . . TokPos: XXX/main.go:5:1
25 . . . Tok: const
26 . . . Lparen: XXX/main.go:5:7
27 . . . Specs: []ast.Spec (len = 3) { // 共声明了3个常量
28 . . . . 0: *ast.ValueSpec {
29 . . . . . Names: []*ast.Ident (len = 1) {
30 . . . . . . 0: *ast.Ident {
31 . . . . . . . NamePos: XXX/main.go:6:2
32 . . . . . . . Name: "one" // 常量名字
33 . . . . . . . Obj: *ast.Object {
34 . . . . . . . . Kind: const
35 . . . . . . . . Name: "one"
36 . . . . . . . . Decl: *(obj @ 28)
37 . . . . . . . . Data: 0 // “只是iota的值”,后续复杂场景可以理解这句话的意思
38 . . . . . . . }
39 . . . . . . }
40 . . . . . }
41 . . . . . Values: []ast.Expr (len = 1) {
42 . . . . . . 0: *ast.Ident {
43 . . . . . . . NamePos: XXX/main.go:6:10
44 . . . . . . . Name: "iota" // 常量对应的值类型为iota
45 . . . . . . }
46 . . . . . }
47 . . . . }
48 . . . . 1: *ast.ValueSpec { //省略部分字段
49 . . . . . Names: []*ast.Ident (len = 1) {
50 . . . . . . 0: *ast.Ident {
52 . . . . . . . Name: "two"
53 . . . . . . . Obj: *ast.Object {
57 . . . . . . . . Data: 1
58 . . . . . . . }
59 . . . . . . }
60 . . . . . }
61 . . . . . Values: []ast.Expr (len = 1) {
62 . . . . . . 0: *ast.Ident {
64 . . . . . . . Name: "iota"
65 . . . . . . }
66 . . . . . }
67 . . . . }
68 . . . . 2: *ast.ValueSpec {
69 . . . . . Names: []*ast.Ident (len = 1) {
70 . . . . . . 0: *ast.Ident {
72 . . . . . . . Name: "three"
73 . . . . . . . Obj: *ast.Object {
77 . . . . . . . . Data: 2
78 . . . . . . . }
79 . . . . . . }
80 . . . . . }
81 . . . . . Values: []ast.Expr (len = 1) {
82 . . . . . . 0: *ast.Ident {
84 . . . . . . . Name: "iota"
85 . . . . . . }
86 . . . . . }
87 . . . . }
88 . . . }
89 . . . Rparen: XXX/main.go:9:1
90 . . }
91 . . 2: *ast.FuncDecl { // func main
92 . . . Name: *ast.Ident {
93 . . . . NamePos: XXX/main.go:11:6
94 . . . . Name: "main"
95 . . . . Obj: *ast.Object {
96 . . . . . Kind: func
97 . . . . . Name: "main"
98 . . . . . Decl: *(obj @ 91)
99 . . . . }
100 . . . }
101 . . .
148 . . }
149 . }
150 . Scope: *ast.Scope {
151 . . Objects: map[string]*ast.Object (len = 4) {
152 . . . "three": *(obj @ 73)
153 . . . "main": *(obj @ 95)
154 . . . "one": *(obj @ 33)
155 . . . "two": *(obj @ 53)
156 . . }
157 . }
158 . Imports: []*ast.ImportSpec (len = 1) {
159 . . 0: *(obj @ 12)
160 . }
161 . Unresolved: []*ast.Ident (len = 4) {
162 . . 0: *(obj @ 42)
163 . . 1: *(obj @ 62)
164 . . 2: *(obj @ 82)
165 . . 3: *(obj @ 114)
166 . }
167 }
源码剖析
调试Go编译器代码
使用Goland调试Go编译器代码
$GOPATH/src/cmd/compile/main.go
iota值是如何确定的
关注ConstDecl
的处理
先在$GOPATH/src/cmd/compile/internal/noder/noder.go:325
打断点观察一下
上方说了,当前文件“共有3个Top Level Decl”,为什么这里的decls有5个?
实际上是把 Top Level Decl 中的const(XXX)
展平了
这里关注一个新的概念:Group
,从调试信息中可以看出,one
、two
、three
这三个ConstDecl
指向了同一个Group
前面埋了两个坑 cs
、Group
这三个ConstDecl
会按顺序执行这一行代码,并传入cs
l = append(l, p.constDecl(decl, &cs)...)
继续来看p.constDecl
的实现(删除部分源码,仅关注当前场景的逻辑代码,后面会再解析剩余源码)
当前场景下,names
和values
的长度都为1
type constState struct {
group *syntax.Group
typ ir.Ntype
values []ir.Node
iota int64 // 初始值为0
}
func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {
// 当Group为nil 或者 当前处理的 Group 与 cs 缓存的 Group 不相同时,新建一个 cs
// Group,顾名思义,“组”
// 当前场景中 ,`one`、`two`、`three` 被同一个 const ()包裹,属于同一个组
// cs是在外层循环开始时定义的,初始化的 cs 是一个空值,所以不属于任何Group
// cs是通过指针传递的,所以在该函数中新建cs,外层是可以感知到的cs的变化的,并会传递给下一个ConstDecl的p.constDecl方法
// 所以在当前场景中,在处理const one时,会进入该分支,更新Group信息
// 在处理two、three时,由于归属同一Group,不会再进入该分支
// 通过这种方法达到了同一个const Group中iota值自加的目的
if decl.Group == nil || decl.Group != cs.group {
*cs = constState{
group: decl.Group,
}
}
names := p.declNames(ir.OLITERAL, decl.NameList)
typ := p.typeExprOrNil(decl.Type)
var values []ir.Node
if decl.Values != nil {
values = p.exprList(decl.Values)
}
nn := make([]ir.Node, 0, len(names))
for i, n := range names {
if i >= len(values) {
base.Errorf("missing value in const declaration")
break
}
v := values[i]
typecheck.Declare(n, typecheck.DeclContext)
n.Ntype = typ
n.Defn = v
// 在处理one时,iota为初始值0
n.SetIota(cs.iota) // 设置这个Node的iota值
nn = append(nn, ir.NewDecl(p.pos(decl), ir.ODCLCONST, n))
}
if len(values) > len(names) {
base.Errorf("extra expression in const declaration")
}
cs.iota++ // iota值自增,所以在后续继续处理two、three时,对应的值已经更改为1,2
return nn
}
SetIota
实际上就是更改了Node中Offset_字段的值,虽然这个Offset
有些词不达意,其实对于不同的Decl类型,Offset
字段的含义是不一样的,复用该字段,来节省空间。
func (n *Name) SetIota(x int64) { n.Offset_ = x }
继续查看一下返回值nn中的信息
可以简单认为,将该Decl节点换了一种形式,我们关注的信息都还在,这个符号的名字是one
,值是iota
,iota
的真实值Offset
为0。
另外注意下,Defn
中的sym
字段,该字段是个指针,它的Def
后续会用来标识和计算iota
的值,当前还为空,记住,Defn
中的sym字
段是个指针,后续还有个骚操作。
再继续看一下func (p *noder) decls
函数的返回值
这里这有4个Node,是因为对于syntax.ImportDecl
,没有append加入到返回值中。
第一个Node上方已观察过,这里只关注第二个和第三个Node的信息,最后一个Node为func main,暂时也不关注。
iota值的读取
继续执行编译流程,关注一下DeclareUniverse
函数,在这个函数中会将上方说到的Defn->sym->Def
字段统一赋值。
func DeclareUniverse() {
// Operationally, this is similar to a dot import of builtinpkg, except
// that we silently skip symbols that are already declared in the
// package block rather than emitting a redeclared symbol error.
for _, s := range types.BuiltinPkg.Syms {
if s.Def == nil {
continue
}
s1 := Lookup(s.Name)
if s1.Def != nil {
continue
}
s1.Def = s.Def
s1.Block = s.Block
}
}
这里其实对于Go中的所有内建(builtin)类型进行了处理,看下Go的内建类型都有哪些,这里应该和$GOPATH/src/builtin/builtin.go
是可以对应的。
Defn->sym->Def
赋值后的信息如下,名字仍为iota
,类型为OIOTA
,后续会根据该信息进行进一步处理。
在func typecheckdef(n *ir.Name)
中拿到Defn
进行进一步操作,注意,这里取出后就设置为nil
清空了。
e := n.Defn
n.Defn = nil
if e == nil {
ir.Dump("typecheckdef nil defn", n)
base.ErrorfAt(n.Pos(), "xxx")
}
e = Expr(e)
接着到达func Resolve(n ir.Node) (res ir.Node)
函数
该函数会对OIOTA
类型的节点,取出其对应的真实值,并返回一个Int节点,进行后续的操作。
将iota
的值取出的正常流程也比较简单从Offest
字段直接获取即可,另外一个if分支我当前还不清楚是什么场景,先留个坑。
func getIotaValue() int64 {
if i := len(typecheckdefstack); i > 0 {
if x := typecheckdefstack[i-1]; x.Op() == ir.OLITERAL {
// 当前场景走这个分支
return x.Iota()
}
}
if ir.CurFunc != nil && ir.CurFunc.Iota >= 0 {
return ir.CurFunc.Iota
}
return -1
}
func (n *Name) Iota() int64 { return n.Offset_ }
至此,iota
类型的生成,变换,读取的逻辑已经都找到了,iota
这个语法糖也已经变为正常的int
值,可以进行后续的正常处理了。
再来回顾一下iota值计算前后的Decls的状态
处理前:
const two和three的val为空。
处理后:
Group的生成和划分
还有一个坑要填,上方说的Group是如何生成和划分的?
这个其实是在Parser阶段就已经划分好了
找到 const
关键字时的处理
继续处理的逻辑,其实比较简单
- 如果const后面有
(
,那么创建一个新的Group
,继续向后扫描,直到找到与其匹配的)
- 如果const后面没有
(
,那么传入的Group
为空
// ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .
func (p *parser) constDecl(group *Group) Decl {
if trace {
defer p.trace("constDecl")()
}
d := new(ConstDecl)
d.pos = p.pos()
d.Group = group
d.Pragma = p.takePragma()
d.NameList = p.nameList(p.name())
if p.tok != _EOF && p.tok != _Semi && p.tok != _Rparen {
d.Type = p.typeOrNil()
if p.gotAssign() {
d.Values = p.exprList()
}
}
return d
}
当前token位置在one
的位置,是名字信息,调用p.name()
后,token会向后走,在gotAssign
中取到=
,p.exprList()
中取到value
解析完成后的结果为,根据地址可以看出,归属于同一个Group
后续的内容就可以和上方的衔接上了。
几个额外的场景
不显式写出iota
package main
import "fmt"
const (
zero = iota
one
two
)
func main() {
fmt.Println(zero, one, two)
}
// Output:
// 0 1 2
观察一下func (p *noder) decls
中decls
中的情况(计算iota值之前)
只有zero的Value是iota,one、two的Value都为空,且这三个常量属于同一个Group
再次解析一下constDecl
函数中相关的逻辑代码
Note:处理zero
、one
、two
的过程中会进入该函数三次
func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {
// 处理zero时,新建一个constState
if decl.Group == nil || decl.Group != cs.group {
*cs = constState{
group: decl.Group,
}
}
names := p.declNames(ir.OLITERAL, decl.NameList)
typ := p.typeExprOrNil(decl.Type)
var values []ir.Node
if decl.Values != nil {
// 处理zero时,Values不为空,将zero的typ和values信息缓存到cs中
values = p.exprList(decl.Values)
cs.typ, cs.values = typ, values
} else {
// 处理two、three时.Values为空
// 读取cs中缓存的typ和values信息(即与zero的typ和values信息相同)
// 达到”继承“上一个显式所写的 iota 表达式的目的
if typ != nil {
base.Errorf("const declaration cannot have type without expression")
}
typ, values = cs.typ, cs.values
}
cs.iota++
return nn
}
const值是和iota相关的表达式
package main
import "fmt"
const (
test1 = iota + 2
test2
)
func main() {
fmt.Println(test1, test2)
}
// Output:
// 2 3
在AST中存储的结构为:
与上方所述流程不同的是,在将iota替换为对应的值后,还有一个二元表达式加法的计算和替换过程
func EvalConst(n ir.Node) ir.Node {
// Pick off just the opcodes that can be constant evaluated.
switch n.Op() { //
case ir.OADD, ir.OSUB, ir.OMUL, ir.ODIV, ir.OMOD, ir.OOR, ir.OXOR, ir.OAND, ir.OANDNOT:
n := n.(*ir.BinaryExpr)
nl, nr := n.X, n.Y // 获取二元表达式的两个操作数
if nl.Op() == ir.OLITERAL && nr.Op() == ir.OLITERAL {
rval := nr.Val()
// check for divisor underflow in complex division (see issue 20227)
if n.Op() == ir.ODIV && n.Type().IsComplex() && constant.Sign(square(constant.Real(rval))) == 0 && constant.Sign(square(constant.Imag(rval))) == 0 {
base.Errorf("complex division by zero")
n.SetType(nil)
return n
}
if (n.Op() == ir.ODIV || n.Op() == ir.OMOD) && constant.Sign(rval) == 0 {
base.Errorf("division by zero")
n.SetType(nil)
return n
}
tok := tokenForOp[n.Op()]
if n.Op() == ir.ODIV && n.Type().IsInteger() {
tok = token.QUO_ASSIGN // integer division
}
return OrigConst(n, constant.BinaryOp(nl.Val(), tok, rval))
}
return n
}
// OrigConst returns an OLITERAL with orig n and value v.
func OrigConst(n ir.Node, v constant.Value) ir.Node {
lno := ir.SetPos(n)
v = convertVal(v, n.Type(), false)
base.Pos = lno
switch v.Kind() {
case constant.Int:
if constant.BitLen(v) <= ir.ConstPrec {
break
}
fallthrough
case constant.Unknown:
what := overflowNames[n.Op()]
if what == "" {
base.Fatalf("unexpected overflow: %v", n.Op())
}
base.ErrorfAt(n.Pos(), "constant %v overflow", what)
n.SetType(nil)
return n
}
// 返回一个新的ConstExpr Node
return ir.NewConstExpr(v, n)
}
func BinaryOp(x_ Value, op token.Token, y_ Value) Value {
x, y := match(x_, y_)
switch x := x.(type) {
case unknownVal:
return x
case int64Val:
a := int64(x)
b := int64(y.(int64Val))
var c int64
switch op {
case token.ADD:
c = a + b // 计算Add之后的值
}
return int64Val(c)
}
}
同一行中有多个常量
package main
import "fmt"
const (
test1, test2 = iota + 1, iota + 2
test3, test4 = iota + 3, iota + 4
)
func main() {
fmt.Println(test1, test2, test3, test4)
}
// Output:
// 1 2 4 5
实际上 test1、test2属于同一个Decl、test3、test4属于同一个Decl,当然它们都属于同一个Group。
上方的例子中,NameList
和Values
中都是只有一个值,实际上它是可以存储多个值的。
再次回到constDecl
函数中相关的逻辑代码
这个例子中会进入该函数两次,第一次时NameList
为[test1,test2]
,第二次时NameList
为 [test3,test4]
func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {
if decl.Group == nil || decl.Group != cs.group {
*cs = constState{
// cs.iota 初始值为0
group: decl.Group,
}
}
names := p.declNames(ir.OLITERAL, decl.NameList)
typ := p.typeExprOrNil(decl.Type)
var values []ir.Node
nn := make([]ir.Node, 0, len(names))
// 遍历NameList中的所有值
for i, n := range names {
if i >= len(values) {
base.Errorf("missing value in const declaration")
break
}
v := values[i]
if decl.Values == nil {
v = ir.DeepCopy(n.Pos(), v)
}
typecheck.Declare(n, typecheck.DeclContext)
n.Ntype = typ
n.Defn = v
n.SetIota(cs.iota) // 将其都设置为cs.iota的当前值
nn = append(nn, ir.NewDecl(p.pos(decl), ir.ODCLCONST, n))
}
// 更新cs.iota
cs.iota++
return nn
}
计算iota
后,对应的值如下
const组中存在_
的情况
package main
import "fmt"
const (
zero = iota
one
two
three
_
five = iota
)
func main() {
fmt.Println(zero, one, two, three, five)
}
// Output:
// 0 1 2 3 5
分析过程与上述相似,读者可自行分析。
const组中第一行不是iota
package main
import "fmt"
const (
five = 5
two = iota
three = iota
)
func main() {
fmt.Println(five, two, three)
}
// Output:
// 5 1 2
分析过程与上述相似,读者可自行分析。
总结
分析过iota
的源码之后,如何人工debug iota
的值呢?
-
先分组(Group):每组的iota值是相互独立的,初始值为0
-
再分行(line):每组中的iota按行从0开始自增
-
确定每一行的iota值:同一行中iota的是相同的
-
确定每一行和iota相关表达式的值:再计算诸如iota+1的具体值
本文由 LeonardWang 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Sep 26,2022