Go 語言 - 類型推斷解析 [2]

若干年後的糾葛,從茫茫戈壁上的相遇開始,早已冥冥註定...

類型推斷依賴編譯器的處理能力,編譯器執行的過程爲:詞法解析 => 語法分析 => 類型檢查 => 中間代碼 => 代碼優化 => 生成機器碼。編譯階段的代碼位於 go/src/cmd/compile 文件中

詞法解析與語法分析階段

在詞法解析階段,會將賦值語句右邊的常量解析爲一個未定義的類型,例如,ImagLit 代表複數,FloatLit 代表浮點數,IntLit 代表整數。

//go/src/cmd/compile/internal/syntax
const (
 IntLit LitKind = iota
 FloatLit
 ImagLit
 RuneLit
 StringLit
)

Go 語言源代碼採用 UTF-8 的編碼方式,在進行詞法解析時當,遇到需要賦值的常量操作時,會逐個讀取後面常量的 UTF-8 字符。字符串的首字符爲 ",數字的首字符爲'0'~'9'。實現位於 syntax.next 函數中。

// go/src/cmd/compile/internal/syntax
func (s *scanner) next() {
...
switch c {
    case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
        s.number(c)
    case '"':
        s.stdString()
    case '`':
        s.rawString()
    ...

因此對於整數、小數等常量的識別就顯得非常簡單。如圖 3-2 所示,整數就是字符中全是 “0”~“9” 的數字,浮點數就是字符中有 “.” 號的數字,字符串的首字符爲 "或'。

圖  詞法解析階段解析未定義的常量

下面列出的 number 函數爲語法分析階段處理數字的具體實現。數字首先會分被爲小數部分與整數部分,通過字符. 進行區分。如果整數部分是以 0 開頭的,則可能有不同的含義,例如 0x 代表十六進制數、0b 代表二進制數。

// go/src/cmd/compile/internal/syntax
func (s *scanner) number(c rune) {
    // 整數部分
    var ds int
    if c != '.' {
        s.kind = IntLit
        if c == '0' {
            c = s.getr()
            switch lower(c) {
            case 'x':
                c = s.getr()
                base, prefix = 16, 'x'
            case 'o':
                c = s.getr()
                base, prefix = 8, 'o'
            case 'b':
                c = s.getr()
                base, prefix = 2, 'b'
            }
        }
        c, ds = s.digits(c, base, &invalid)
        digsep |= ds
    }
    // 小數部分
    if c == '.' {
        s.kind = FloatLit
        c, ds = s.digits(s.getr(), base, &invalid)
        digsep |= ds
    }

以賦值語句 a := 333 爲例, 完成詞法解析與語法分析時, 此賦值語句將以 AssignStmt 結構表示。

AssignStmt struct {
        Op       Operator 
        Lhs, Rhs Expr
        simpleStmt
    }

其中 Op 代表操作符,在這裏是賦值操作 OAS。Lhs 與 Rhs 分別代表左右兩個表達式,左邊代表變量 a,右邊代表常量 333,此時其類型爲 intLit。

抽象語法樹生成與類型檢查

完成語法解析後,進入抽象語法樹階段。在該階段會將詞法解析階段生成的解析爲一個 Node,Node 結構體是對抽象語法樹中節點的抽象。

type Node struct {
    Left  *Node
    Right *Node
    Ninit Nodes
    Nbody Nodes
    List  Nodes
    Rlist Nodes
    Type *types.Type
    E   interface{} 
    ...

其中,Left(左節點)代表左邊的變 a,Right(右節點)代表整數 333,其 Op 操作爲。Right 的 E 接口字段會存儲值 333,如果前一階段爲 IntLit 類型,則需要轉換爲 Mpint 類型。Mpint 類型用於存儲整數常量,具體結構如下所示。

// Mpint 代表整數常量
type Mpint struct {
    Val  big.Int
    Ovf  bool 
    Rune bool 
}

從 Mpint 類型的結構可以看到,在編譯時 AST 階段整數通過 math/big.Int 進行高精度存儲,浮點數通過 big.Float 進行高精度存儲

在類型檢查階段,右節點中的 Type 字段存儲的類型會變爲 types.Types[TINT]。存儲了不同標識對應的 Go 語言中的實際類型,其中,types.Types[TINT] 對應 Go 語言內置的 int 類型。

接着完成最終的賦值操作,並將右邊常量的類型賦值給左邊變量的類型。具體實現位於 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
        }
    }
...
}

在 SSA 階段,變量 a 中存儲的大數類型的 333 最終會調用 big.Int 包中 Int64 函數將其轉換爲 int64 類型的常量,形如:v4 (?) = MOVQconst [333] (a[int])。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SURKglDlaAQhbuJkzBYnKQ