乐趣区

关于后端:Go编译原理系列6类型检查

6. Go 编译过程 - 类型查看

前言

在前边的一篇文章中分享了形象语法树的构建,下边的一个阶段就是类型查看,它会遍历每一个形象语法树的结点,会依照如下步骤对不同类型的结点进行类型查看(动态类型查看):

  • 常量、类型和函数名及类型验证
  • 变量的赋值和初始化
  • 计算编译时的常量、将申明与标识符绑定
  • 会对一些内置函数进行改写(下边介绍源码时会提到)
  • 哈希键值对的类型
  • 做特地的语法或语义查看(援用的构造体字段是否是大写可导出的?数组字面量的拜访是否超过了其长度?数组的索引是不是正整数?)

通过类型查看,它能够保障每一个形象语法树的结点不会呈现类型谬误(留神,编译阶段是动态类型查看),源代码中的动态类型谬误,会在类型查看的过程中被发现。并且,如果某个类型是否实现了某个接口,也会在该阶段被查看进去

通过本文你能够理解到 Go 的类型查看阶段都做了哪些事件,以及在查看一些非凡类型的结点时,对结点做了哪些非凡的改写(比方:map、make)

类型查看整体概览

类型查看阶段会遍历形象语法树的每一个节点,确定节点的类型。例如下边这两种模式

var test int
test := 1

第一种是间接指定了类型,第二种是须要编译器通过类型推断来失去变量的类型

在前边的几篇文章中提到了,Go 编译的入口文件在:

Go 的编译入口文件:src/cmd/compile/main.go -> gc.Main(archInit)

进入到 gc.Main(archInit)办法,你会看到,执行完词法剖析、语法分析、形象语法树的构建之后,有下边这么一段代码:

func Main(archInit func(*Arch)) {
    ......
    lines := parseFiles(flag.Args())// 词法剖析、语法分析、形象语法树构建都在这里
    ......
    // 开始遍历形象语法树,对每个结点进行类型查看
    for i := 0; i < len(xtop); i++ {n := xtop[i]
        if op := n.Op; op != ODCL && op != OAS && op != OAS2 && (op != ODCLTYPE || !n.Left.Name.Param.Alias()) {xtop[i] = typecheck(n, ctxStmt)
        }
    }
    for i := 0; i < len(xtop); i++ {n := xtop[i]
        if op := n.Op; op == ODCL || op == OAS || op == OAS2 || op == ODCLTYPE && n.Left.Name.Param.Alias() {xtop[i] = typecheck(n, ctxStmt)
        }
    }
    ......
    checkMapKeys()// 对哈希中键的类型进行查看
    ......
}

这里的 xtop 是一个数组,它里边是每一棵形象语法树的根节点(在形象语法树构建这篇文章中提到,它会将每一种申明语句都构建成一棵形象语法树,比方 var、const、type、func 等)。因而它会从每一棵树的根节点开始遍历,逐个进行类型查看

从上边的代码中咱们能够看到,类型查看次要是调用了:/usr/local/go/src/cmd/compile/internal/gc/typecheck.go→typecheck办法,该办法会对常量、类型、函数申明、赋值语句等进行类型查看。同时后边调用了 checkMapKeys()办法对哈希的键进行类型查看(下边会对这两个办法的实现进行具体的介绍)

其实在 typecheck 办法中,外围逻辑在它调用的 typecheck1 办法中。该办法中由一个很大的 switch 形成,依据每个节点的 Op,来抉择不同的解决逻辑。这里边分支十分多,我这里仅抉择几个比拟特地的进行深刻的理解

func typecheck1(n *Node, top int) (res *Node) {
        ......
        switch n.Op {
        // until typecheck is complete, do nothing.
        default:
            Dump("typecheck", n)
    
            Fatalf("typecheck %v", n.Op)
    
        // names
        case OLITERAL:
            ok |= ctxExpr
    
            if n.Type == nil && n.Val().Ctype() == CTSTR {n.Type = types.UntypedString}
    
        case ONONAME:
            ok |= ctxExpr
    
        case ONAME:
            ......
        case OTARRAY:
        ......
        case OTMAP:
        ......
        }
        ......
}

深刻理解类型查看

OAS:赋值语句

// Left = Right or (if Colas=true) Left := Right
// If Colas, then Ninit includes a DCL node for Left.
OAS

赋值语句的外围是调用了:/usr/local/go/src/cmd/compile/internal/gc/typecheck.go→typecheckas

case OAS:
        ok |= ctxStmt

        typecheckas(n)

        // Code that creates temps does not bother to set defn, so do it here.
        if n.Left.Op == ONAME && n.Left.IsAutoTmp() {n.Left.Name.Defn = n}

typecheckas 办法中,能够看到如下这段代码,它是将左边常量的类型,赋值给右边变量的类型

func typecheckas(n *Node) {
        ......
        if n.Left.Name != nil && n.Left.Name.Defn == n && n.Left.Name.Param.Ntype == nil {n.Right = defaultlit(n.Right, nil)
                n.Left.Type = n.Right.Type
        }
        ......
}

比方:var a = 666

OTARRAY:切片

OTARRAY // []int, [8]int, [N]int or [...]int

对于节点类型是 OTARRAY 的状况,它会先查看切片值的类型(该节点的右节点)

case OTARRAY:
        ok |= ctxType
        r := typecheck(n.Right, ctxType)
        if r.Type == nil {
            n.Type = nil
            return n
        }

而后依据右边节点的不同,分三种状况,即[]int、[…]int、[6]int

  • []int:间接调用t = types.NewSlice(r.Type),返回了一个 TSLICE 类型的构造体,元素的类型信息也会存储在构造体中
  • […]int:交由 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
        
                length := typecheckarraylit(elemType, -1, n.List.Slice(), "array literal")
        
                n.Op = OARRAYLIT
                n.Type = types.NewArray(elemType, length)
                n.Right = nil
                return n
        }
        ......
}

该办法会获取到数组中元素的数量,而后调用[types.NewArray](https://draveness.me/golang/tree/cmd/compile/internal/types.NewArray) 初始化一个存储着数组中元素类型和数组大小的构造体

  • [6]int:如果在申明切片时,带了数组的大小,则间接调用[types.NewArray](https://draveness.me/golang/tree/cmd/compile/internal/types.NewArray) 初始化一个存储着数组中元素类型和数组大小的构造体

能够发现数组的长度是类型查看期间确定的

在最初,它会更新该节点的 Type 等信息

setTypeNode(n, t)
n.Left = nil
n.Right = nil
checkwidth(t)

OTMAP:map(哈希)

OTMAP    // map[string]int

对于 OTMAP 类型的结点,它会别离对左右两边的局部进行类型查看,而后创立一个 TMAP 构造体,将 MAP 的键值类型存到该构造体中

case OTMAP:
        ok |= ctxType
        n.Left = typecheck(n.Left, ctxType)
        n.Right = typecheck(n.Right, ctxType)
        l := n.Left
        r := n.Right
        if l.Type == nil || r.Type == nil {
            n.Type = nil
            return n
        }
        if l.Type.NotInHeap() {yyerror("incomplete (or unallocatable) map key not allowed")
        }
        if r.Type.NotInHeap() {yyerror("incomplete (or unallocatable) map value not allowed")
        }

        setTypeNode(n, types.NewMap(l.Type, r.Type))
        mapqueue = append(mapqueue, n) // check map keys when all types are settled
        n.Left = nil
        n.Right = nil

......
func NewMap(k, v *Type) *Type {t := New(TMAP)
    mt := t.MapType()
    mt.Key = k
    mt.Elem = v
    return t
}

咱们从代码中能够发现,它不仅对结点进行了批改,而且将结点放入了一个 mapqueue 队列。在前边的概览局部提到了 checkMapKeys()会对哈希键的类型进行再次的查看

func checkMapKeys() {
    for _, n := range mapqueue {k := n.Type.MapType().Key
        if !k.Broke() && !IsComparable(k) {yyerrorl(n.Pos, "invalid map key type %v", k)
        }
    }
    mapqueue = nil
}

其实就是遍历队列,验证这些类型是否能够作为 map 的 key

OMAKE:make

OMAKE          // make(List) (before type checking converts to one of the following)
OMAKECHAN      // make(Type, Left) (type is chan)
OMAKEMAP       // make(Type, Left) (type is map)
OMAKESLICE     // make(Type, Left, Right) (type is slice)

在编写 go 代码时,咱们常常会用到 make 关键字来创立 slice、map、channel 等,在 Go 编译的类型查看阶段,它会细分 OMAKE 的结点,比方:

make slice:OMAKESLICE
make map:OMAKEMAP
make chan:OMAKECHAN

具体实现就是,它会先获取到 make 的第一个参数,也就是类型。依据这个类型,进行不同的解决

case OMAKE:
        ok |= ctxExpr
        args := n.List.Slice()
        ......
        l := args[0]
        l = typecheck(l, ctxType)
        t := l.Type
        ......
        i := 1
        switch t.Etype {
        default:
            yyerror("cannot make type %v", t)
            n.Type = nil
            return n

        case TSLICE:
            ......
            n.Left = l
            n.Right = r
            n.Op = OMAKESLICE

        case TMAP:
            ......
            n.Op = OMAKEMAP
        case TCHAN:
            ......
            n.Op = OMAKECHAN
        }

        n.Type = t
  • 如果第一个参数是 切片类型 :获取切片的长度(len) 和容量(cap),而后对 len 和 cap 进行合法性校验。并且改写了节点的类型
  • 如果第一个参数是map 类型:获取 make 的第二个参数,如果没有,则默认设置为 0(map 的大小)。并且改写节点的类型
  • 如果第一个参数是 chan类型:获取 make 的第二个参数,如果没有,则默认设置为 0(chan 的缓冲区大小)。并且改写节点的类型

我这里没粘代码了,大家能够自行去看

总结

本文次要是分享了类型查看中的几个非凡的节点类型。还有很多其它类型的节点的类型查看,大家能够自行的去看源码

在前边几篇文章中没有分享 Go 的源码调试,所以下篇文章打算分享 Go 的源码调试形式。并且以形象语法树的构建为例,对它进行调试

参考

  • go-ast-book
  • 《Go 语言底层原理分析》
  • 面向信奉编程 - 类型查看
退出移动版