Go 语言的编译器入口在 src/cmd/compile/internal/gc/main.go 的 Main 函数。
编译器类型检查主要逻辑在 src/cmd/compile/internal/gc/typecheck.go 的 typecheck 函数中。
我们都知道,Go 语言数组在初始化之后大小就无法改变了,元素类型相同但大小不同的数组类型在 Go 看来是不同类型。
在 Go 里面,数组的初始化有两种方式,这两种声明方式在运行期间得到的结果相同,后面一种在编译期间会转换成前一种。
a := [3]{1,2,3}
a := [...]{1,2,3}
复制代码
第一种的编译期处理方式如下:
// src/cmd/compile/internal/types/type.go.NewArray
// NewArray returns a new fixed-length array Type.
func NewArray(elem *Type, bound int64) *Type {
if bound < 0 {
Fatalf("NewArray: invalid bound %v", bound)
}
t := New(TARRAY)
t.Extra = &Array{Elem: elem, Bound: bound}
t.SetNotInHeap(elem.NotInHeap())
return t
}
复制代码
第二种的处理会稍微复杂一些,它对应的类型是OCOMPLIT
,而第一种的类型是OTARRAY
。处理方式如下:
//src/cmd/compile/internal/gc/typecheck.go.typecheckcomplit
func typecheckcomplit(n *Node) (res *Node) {
...
// Need to handle [...]T arrays specially.
if n.Right.Op == OTARRAY && n.Right.Left != nil && n.Right.Left.Op == ODDD {
n.Right.Right = typecheck(n.Right.Right, ctxType)
if n.Right.Right.Type == nil {
n.Type = nil
return n
}
elemType := n.Right.Right.Type
//通过调用typecheckarraylit来获取数组中元素的数量,就可以转化成第一种声明方式了
length := typecheckarraylit(elemType, -1, n.List.Slice(), "array literal")
n.Op = OARRAYLIT
n.Type = types.NewArray(elemType, length)
n.Right = nil
return n
}
...
switch t.Etype {
case TARRAY:
typecheckarraylit(t.Elem(), t.NumElem(), n.List.Slice(), "array literal")
n.Op = OARRAYLIT
n.Right = nil
...
}
复制代码
除了声明方式、类型检查,数组越界也是数组一个很重要的特性,有一些错误我们是可以在编译期发现
//src/cmd/compile/internal/gc/typecheck.go.typecheck1
case OINDEX:
ok |= ctxExpr
n.Left = typecheck(n.Left, ctxExpr)
n.Left = defaultlit(n.Left, nil)
n.Left = implicitstar(n.Left)
l := n.Left
n.Right = typecheck(n.Right, ctxExpr)
r := n.Right
t := l.Type
if t == nil || r.Type == nil {
n.Type = nil
return n
}
switch t.Etype {
default:
yyerror("invalid operation: %v (type %v does not support indexing)", n, t)
n.Type = nil
return n
case TSTRING, TARRAY, TSLICE:
n.Right = indexlit(n.Right)
if t.IsString() {
n.Type = types.Bytetype
} else {
n.Type = t.Elem()
}
why := "string"
if t.IsArray() {
why = "array"
} else if t.IsSlice() {
why = "slice"
}
if n.Right.Type != nil && !n.Right.Type.IsInteger() {
yyerror("non-integer %s index %v", why, n.Right)
break
}
if !n.Bounded() && Isconst(n.Right, CTINT) {
x := n.Right.Int64Val()
if x < 0 {
yyerror("invalid %s index %v (index must be non-negative)", why, n.Right)
} else if t.IsArray() && x >= t.NumElem() {
yyerror("invalid array index %v (out of bounds for %d-element array)", n.Right, t.NumElem())
} else if Isconst(n.Left, CTSTR) && x >= int64(len(n.Left.StringVal())) {
yyerror("invalid string index %v (out of bounds for %d-byte string)", n.Right, len(n.Left.StringVal()))
} else if n.Right.Val().U.(*Mpint).Cmp(maxintval[TINT]) > 0 {
yyerror("invalid %s index %v (index too large)", why, n.Right)
}
}
复制代码
可以看到核心逻辑主要为非整数检查、非负数检查、数组越界检查。
如果在编辑期间无法判断是否可能会越界(比如下标是由参数传入的),则会在生成的 SSA 指令(生成的中间代码里)带上判断是否越界的操作(也就是说会把检查函数插入到运行期间执行),并且从 SSA 代码里可以看出,无论是数组寻址还是赋值,都是在编译期执行的,没有运行时的参与。
SSA 生成指令(GOSSA=func_name go build main.go)
评论