Go編譯原理系列5(抽象語法樹構建)

書旅發表於2022-01-15

前言

在上一篇語法分析中,我們知道了Go編譯器是如何按照Go的文法,解析go文字檔案中的各種宣告型別(import、var、const、func等)。語法分析階段將整個原始檔解析到一個File的結構體中,原始檔中各種宣告型別解析到File.DeclList中。最終生成以File結構體為根節點,importDeclconstDecltypeDeclvarDeclFuncDecl等為子節點的語法樹

首先我們需要明確的就是,抽象語法樹的作用其實就是為了後邊進行型別檢查、程式碼風格檢查等等。總之,有了抽象語法樹,編譯器就可以精準的定位到程式碼的任何地方,對其進行一些列的操作及驗證等

本文為抽象語法樹的構建,我們知道,在編譯器前端必須將源程式構建成一種中間表示形式,以便在編譯器的後端進行使用,抽象語法樹就是一種常見的樹狀的中間表示形式。所以本文主要是介紹Go編譯器將語法樹構建成抽象語法樹都做了哪些事情?

抽象語法樹構建概覽

以下先從整體上認識抽象語法樹構建過程,可能跨度比較大,具體實現細節在下一部分介紹

在上一篇的語法解析階段我們知道,Go編譯器會起多個協程,將每一個原始檔都解析成一棵語法樹。具體程式碼的位置是:src/cmd/compile/internal/gc/noder.go → parseFiles

func parseFiles(filenames []string) uint {
    noders := make([]*noder, 0, len(filenames))
    // Limit the number of simultaneously open files.
    sem := make(chan struct{}, runtime.GOMAXPROCS(0)+10)

    for _, filename := range filenames {
        p := &noder{
            basemap: make(map[*syntax.PosBase]*src.PosBase),
            err:     make(chan syntax.Error),
        }
        noders = append(noders, p)
        //起多個協程對原始檔進行語法解析
        go func(filename string) {
            sem <- struct{}{}
            defer func() { <-sem }()
            defer close(p.err)
            base := syntax.NewFileBase(filename)

            f, err := os.Open(filename)
            if err != nil {
                p.error(syntax.Error{Msg: err.Error()})
                return
            }
            defer f.Close()

            p.file, _ = syntax.Parse(base, f, p.error, p.pragma, syntax.CheckBranches) // errors are tracked via p.error
        }(filename)
    }

  //開始將每一棵語法樹構建成抽象語法樹
    var lines uint
    for _, p := range noders {
        for e := range p.err {
            p.yyerrorpos(e.Pos, "%s", e.Msg)
        }

        p.node() //構建抽象語法樹的核心實現
        lines += p.file.Lines
        p.file = nil // release memory

        ......
    }

    localpkg.Height = myheight

    return lines
}

在將原始檔解析成一棵語法樹之後,Go編譯器會將每一棵語法樹(原始檔)構建成抽象語法樹。其核心程式碼就在p.node()這個方法中:

func (p *noder) node() {
    ......

    xtop = append(xtop, p.decls(p.file.DeclList)...)

    ......
    clearImports()
}

p.node()這個方法的核心部分是p.decls(p.file.DeclList)方法,該方法實現了將原始檔中的各種宣告型別轉換成一個一個的抽象語法樹,即import、var、type、const、func宣告都會成為一個根節點,根節點下邊包含當前宣告的子節點

p.decls(p.file.DeclList)的實現如下:

func (p *noder) decls(decls []syntax.Decl) (l []*Node) {
    var cs constState

    for _, decl := range decls {
        p.setlineno(decl)
        switch decl := decl.(type) {
        case *syntax.ImportDecl:
            p.importDecl(decl)

        case *syntax.VarDecl:
            l = append(l, p.varDecl(decl)...)

        case *syntax.ConstDecl:
            l = append(l, p.constDecl(decl, &cs)...)

        case *syntax.TypeDecl:
            l = append(l, p.typeDecl(decl))

        case *syntax.FuncDecl:
            l = append(l, p.funcDecl(decl))

        default:
            panic("unhandled Decl")
        }
    }

    return
}

從整體上來看,該方法其實就是將語法樹中的各種宣告型別,都轉換成了以各種宣告為根節點的抽象語法樹(Node結構),最終語法樹就變成了一個節點陣列(Node)

下邊可以看一下這個Node結構體長什麼樣

type Node struct {
    // Tree structure.
    // Generic recursive walks should follow these fields.
    //通用的遞迴遍歷,應該遵循這些欄位
    Left  *Node //左子節點
    Right *Node //右子節點
    Ninit Nodes
    Nbody Nodes
    List  Nodes //左子樹
    Rlist Nodes //右子樹

    // most nodes
    Type *types.Type //節點型別
    Orig *Node // original form, for printing, and tracking copies of ONAMEs

    // func
    Func *Func //方法

    // ONAME, OTYPE, OPACK, OLABEL, some OLITERAL
    Name *Name //變數名、型別明、包名等等

    Sym *types.Sym  // various
    E   interface{} // Opt or Val, see methods below

    // Various. Usually an offset into a struct. For example:
    // - ONAME nodes that refer to local variables use it to identify their stack frame position.
    // - ODOT, ODOTPTR, and ORESULT use it to indicate offset relative to their base address.
    // - OSTRUCTKEY uses it to store the named field's offset.
    // - Named OLITERALs use it to store their ambient iota value.
    // - OINLMARK stores an index into the inlTree data structure.
    // - OCLOSURE uses it to store ambient iota value, if any.
    // Possibly still more uses. If you find any, document them.
    Xoffset int64

    Pos src.XPos

    flags bitset32

    Esc uint16 // EscXXX

    Op  Op //當前結點的屬性
    aux uint8
}

知道上邊註釋中的幾個欄位的含義,基本就夠用了。核心是Op這個欄位,它標識了每個結點的屬性。你可以在:src/cmd/compile/internal/gc/syntax.go中看到所有Op的定義,它都是以O開頭的,也都是整數,每個Op都有自己的語義

const (
    OXXX Op = iota

    // names
    ONAME // var or func name 遍歷名或方法名
    // Unnamed arg or return value: f(int, string) (int, error) { etc }
    // Also used for a qualified package identifier that hasn't been resolved yet.
    ONONAME
    OTYPE    // type name 變數型別
    OPACK    // import
    OLITERAL // literal 識別符號

    // expressions
    OADD          // Left + Right  加法
    OSUB          // Left - Right  減法
    OOR           // Left | Right  或運算
    OXOR          // Left ^ Right
    OADDSTR       // +{List} (string addition, list elements are strings)
    OADDR         // &Left
    ......
    // Left = Right or (if Colas=true) Left := Right
    // If Colas, then Ninit includes a DCL node for Left.
    OAS
    // List = Rlist (x, y, z = a, b, c) or (if Colas=true) List := Rlist
    // If Colas, then Ninit includes DCL nodes for List
    OAS2
    OAS2DOTTYPE // List = Right (x, ok = I.(int))
    OAS2FUNC    // List = Right (x, y = f())
  ......
)

比如當某一個節點的Op為OAS,該節點代表的語義就是Left := Right。當節點的Op為OAS2時,代表的語義就是x, y, z = a, b, c

假設有這樣一個宣告語句:a := b + c(6),構建成抽象語法樹就長下邊這樣

最終每種宣告語句都會被構建成這樣的抽象語法樹。上邊是對抽象語法樹有個大致的認識,下邊就具體的看各種宣告語句是如何一步一步的被構建成抽象語法樹的

語法分析階段解析各種宣告

為了更直觀的看到抽象語法樹是如何解析各種宣告的,我們可以直接利用go提供的標準庫中的方法來進行除錯。因為在前邊並沒有直觀的看到一個宣告被語法解析出來之後長什麼樣子,所以下邊通過標準庫中的方法來展示一下

? 提示:在前邊的Go詞法分析這篇文章中提到,Go提供的標準庫中的詞法解析、語法解析、抽象語法樹構建等的實現跟Go編譯器中的實現或設計不一樣,但是整體的思路是一樣的

基礎面值解析

基礎面值有整數浮點數複數字元、字串、識別符號。從上一篇Go的語法解析中知道,Go編譯器中基礎面值的結構體為

BasicLit struct {
        Value string   //值
        Kind  LitKind  //那種型別的基礎面值,範圍(IntLit、FloatLit、ImagLit、RuneLit、StringLit)
        Bad   bool // true means the literal Value has syntax errors
        expr
}

而在標準庫中,基礎面值的結構體長下邊這樣

BasicLit struct {
        ValuePos token.Pos   // literal position
        Kind     token.Token // token.INT, token.FLOAT, token.IMAG, token.CHAR, or token.STRING
        Value    string      // literal string; e.g. 42, 0x7f, 3.14, 1e-9, 2.4i, 'a', '\x7f', "foo" or `\m\n\o`
    }

其實幾乎是一樣的,包括我們後邊會提到的其它的各種面值的結構體或宣告的結構體,在Go編譯器中和Go標準庫中結構都不一樣,但是含義都差不多

知道了基礎面值的結構,如果我們想構建一個基礎面值,就可以這樣

func AstBasicLit()  {
    var basicLit = &ast.BasicLit{
        Kind:  token.INT,
        Value: "666",
    }
    ast.Print(nil, basicLit)
}

//列印結果
*ast.BasicLit {
        ValuePos: 0
    Kind: INT
    Value: "666"
}

上邊就是直接構建了一個基礎面值,理論上我們可以按照這種方式構造一個完成的語法樹,但是手工的方式畢竟太麻煩。所以標準庫中提供了方法來自動的構建語法樹。假設我要將整數666構建成基礎面值的結構

func AstBasicLitCreat()  {
    expr, _ := parser.ParseExpr(`666`)
    ast.Print(nil, expr)
}

//列印結果
*ast.BasicLit {
        ValuePos: 1
    Kind: INT
    Value: "666"
}

再比如識別符號面值,它的結構體為:

type Ident struct {
    NamePos token.Pos // 位置
    Name    string    // 識別符號名字
    Obj     *Object   // 識別符號型別或擴充套件資訊
}

通過下邊方法可以構建一個識別符號型別

func AstInent()  {
    ast.Print(nil, ast.NewIdent(`a`))
}

//列印結果
*ast.Ident {
        NamePos: 0
    Name: "a"
}

如果識別符號是出現在一個表示式中,就會通過Obj這個欄位存放識別符號的額外資訊

func AstInent()  {
    expr, _ := parser.ParseExpr(`a`)
    ast.Print(nil, expr)
}

//列印結果
*ast.Ident {
   NamePos: 1
   Name: "a"
   Obj: *ast.Object {
      Kind: bad
      Name: ""
   }
}

ast.Object結構中的Kind是描述識別符號的型別

const (
    Bad ObjKind = iota // for error handling
    Pkg                // package
    Con                // constant
    Typ                // type
    Var                // variable
    Fun                // function or method
    Lbl                // label
)

表示式解析

在標準庫的go/ast/ast.go中,你會看到各種型別的表示式的結構,我這裡粘一部分看一下

// A SelectorExpr node represents an expression followed by a selector.
    SelectorExpr struct {
        X   Expr   // expression
        Sel *Ident // field selector
    }

    // An IndexExpr node represents an expression followed by an index.
    IndexExpr struct {
        X      Expr      // expression
        Lbrack token.Pos // position of "["
        Index  Expr      // index expression
        Rbrack token.Pos // position of "]"
    }

    // A SliceExpr node represents an expression followed by slice indices.
    SliceExpr struct {
        X      Expr      // expression
        Lbrack token.Pos // position of "["
        Low    Expr      // begin of slice range; or nil
        High   Expr      // end of slice range; or nil
        Max    Expr      // maximum capacity of slice; or nil
        Slice3 bool      // true if 3-index slice (2 colons present)
        Rbrack token.Pos // position of "]"
    }

在Go的編譯器中,你也可以看到類似的表示式結構,位置在:src/cmd/compile/internal/gc/noder.go

// X.Sel
    SelectorExpr struct {
        X   Expr
        Sel *Name
        expr
    }

    // X[Index]
    IndexExpr struct {
        X     Expr
        Index Expr
        expr
    }

    // X[Index[0] : Index[1] : Index[2]]
    SliceExpr struct {
        X     Expr
        Index [3]Expr
        // Full indicates whether this is a simple or full slice expression.
        // In a valid AST, this is equivalent to Index[2] != nil.
        // TODO(mdempsky): This is only needed to report the "3-index
        // slice of string" error when Index[2] is missing.
        Full bool
        expr
    }

雖然結構體定義的不一樣,但是表達的含義是差不多的。在標準庫中提供了很多解析各種表示式的方法

type BadExpr struct{ ... }
type BinaryExpr struct{ ... }
type CallExpr struct{ ... }
type Expr interface{ ... }
type ExprStmt struct{ ... }
type IndexExpr struct{ ... }
type KeyValueExpr struct{ ... }
......

而在Go編譯器中,解析表示式的核心方法是:src/cmd/compile/internal/gc/noder.go→expr()

func (p *noder) expr(expr syntax.Expr) *Node {
    p.setlineno(expr)
    switch expr := expr.(type) {
    case nil, *syntax.BadExpr:
        return nil
    case *syntax.Name:
        return p.mkname(expr)
    case *syntax.BasicLit:
        n := nodlit(p.basicLit(expr))
        n.SetDiag(expr.Bad) // avoid follow-on errors if there was a syntax error
        return n
    case *syntax.CompositeLit:
        n := p.nod(expr, OCOMPLIT, nil, nil)
        if expr.Type != nil {
            n.Right = p.expr(expr.Type)
        }
        l := p.exprs(expr.ElemList)
        for i, e := range l {
            l[i] = p.wrapname(expr.ElemList[i], e)
        }
        n.List.Set(l)
        lineno = p.makeXPos(expr.Rbrace)
        return n
    case *syntax.KeyValueExpr:
        // use position of expr.Key rather than of expr (which has position of ':')
        return p.nod(expr.Key, OKEY, p.expr(expr.Key), p.wrapname(expr.Value, p.expr(expr.Value)))
    case *syntax.FuncLit:
        return p.funcLit(expr)
    case *syntax.ParenExpr:
        return p.nod(expr, OPAREN, p.expr(expr.X), nil)
    case *syntax.SelectorExpr:
        // parser.new_dotname
        obj := p.expr(expr.X)
        if obj.Op == OPACK {
            obj.Name.SetUsed(true)
            return importName(obj.Name.Pkg.Lookup(expr.Sel.Value))
        }
        n := nodSym(OXDOT, obj, p.name(expr.Sel))
        n.Pos = p.pos(expr) // lineno may have been changed by p.expr(expr.X)
        return n
    case *syntax.IndexExpr:
        return p.nod(expr, OINDEX, p.expr(expr.X), p.expr(expr.Index))
    
    ......
    }
    panic("unhandled Expr")
}

下邊還是用Go標準庫中提供的方法來看一個二元表示式解析出來之後長啥樣

func AstBasicExpr()  {
    expr, _ := parser.ParseExpr(`6+7*8`)
    ast.Print(nil, expr)
}

首先二元表示式的結構體是BinaryExpr

// A BinaryExpr node represents a binary expression.
    BinaryExpr struct {
        X     Expr        // left operand
        OpPos token.Pos   // position of Op
        Op    token.Token // operator
        Y     Expr        // right operand
    }

當被解析成這樣的結構之後,就可以根據Op的型別來建立不同的節點。在前邊提到,每一個Op都有自己的語義

表示式求值

假設要對上邊的那個二元表示式進行求值

func AstBasicExpr()  {
    expr, _ := parser.ParseExpr(`6+7*8`)
    fmt.Println(Eval(expr))
}

func Eval(exp ast.Expr) float64 {
    switch exp := exp.(type) {
    case *ast.BinaryExpr: //如果是二元表示式型別,呼叫EvalBinaryExpr進行解析
        return EvalBinaryExpr(exp)
    case *ast.BasicLit: //如果是基礎面值型別
        f, _ := strconv.ParseFloat(exp.Value, 64)
        return f
    }
    return 0
}

func EvalBinaryExpr(exp *ast.BinaryExpr) float64 { //這裡僅實現了+和*
    switch exp.Op {
    case token.ADD:
        return Eval(exp.X) + Eval(exp.Y)
    case token.MUL:
        return Eval(exp.X) * Eval(exp.Y)
    }
    return 0
}

//列印結果
62

主要的地方加了註釋,應該是很容易看明白

Var宣告解析

首先需要說明的是,在上一篇Go語法解析中我們知道,對於Var型別的宣告,會解析到VarDecl結構體中。但是在Go標準庫中,語法解析將Var、const、type、import宣告都解析到GenDecl這個結構體中(叫通用宣告)

//    token.IMPORT  *ImportSpec
    //    token.CONST   *ValueSpec
    //    token.TYPE    *TypeSpec
    //    token.VAR     *ValueSpec
    //
    GenDecl struct {
        Doc    *CommentGroup // associated documentation; or nil
        TokPos token.Pos     // position of Tok
        Tok    token.Token   // IMPORT, CONST, TYPE, VAR
        Lparen token.Pos     // position of '(', if any
        Specs  []Spec
        Rparen token.Pos // position of ')', if any
    }

可以通過Tok欄位來區分是哪種型別的宣告

下邊通過一個示例展示Var宣告被語法解析之後的樣子

const srcVar = `package test
var a = 6+7*8
`

func AstVar()  {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "hello.go", srcVar, parser.AllErrors)
    if err != nil {
        log.Fatal(err)
    }
    for _, decl := range f.Decls {
        if v, ok := decl.(*ast.GenDecl); ok {
            fmt.Printf("Tok: %v\n", v.Tok)
            for _, spec := range v.Specs {
                ast.Print(nil, spec)
            }
        }
    }
}

首先可以看到它的Tok是Var,表示他是Var型別的宣告,然後它的變數名通過ast.ValueSpec結構體存放的,其實就可以理解成Go編譯器中的VarDecl結構體

到這裡就應該大致瞭解了基礎面值、表示式、var宣告,經過語法解析之後是什麼樣子。前邊的概覽中提到,抽象語法樹階段會將Go原始檔中的各種宣告,轉換成一個一個的抽象語法樹,即import、var、type、const、func宣告都會成為一個根節點,根節點下邊包含當前宣告的子節點。下邊就以var宣告為例,看一下它是如何進行處理的

抽象語法樹構建

每種宣告的抽象語法樹構建過程的思路差不多,裡邊的程式碼比較複雜,就不一行一行的程式碼去解釋它們在做什麼了,大家可以自己去看:src/cmd/compile/internal/gc/noder.go →decls()方法的內部實現

我這裡僅以Var宣告的語句為例,來展示一下在抽象語法樹構建階段是如何處理var宣告的

Var宣告語句的抽象語法樹構建

在前邊已經提到,抽象語法樹構建的核心邏輯在:src/cmd/compile/internal/gc/noder.go →decls,當宣告型別為*syntax.VarDecl時,呼叫p.varDecl(decl)方法進行處理

func (p *noder) decls(decls []syntax.Decl) (l []*Node) {
    var cs constState

    for _, decl := range decls {
        p.setlineno(decl)
        switch decl := decl.(type) {
        ......
        case *syntax.VarDecl:
            l = append(l, p.varDecl(decl)...)
        ......
        default:
            panic("unhandled Decl")
        }
    }

    return
}

下邊直接看p.varDecl(decl)的內部實現

func (p *noder) varDecl(decl *syntax.VarDecl) []*Node {
    names := p.declNames(decl.NameList) //處理變數名
    typ := p.typeExprOrNil(decl.Type) //處理變數型別

    var exprs []*Node
    if decl.Values != nil {
        exprs = p.exprList(decl.Values) //處理值
    }
    ......
    return variter(names, typ, exprs)
}

我將該方法中呼叫的幾個核心方法展示出來了,方法呼叫的都比較深,我下邊會通過圖的方式來展示每個方法裡邊都做了些什麼事情

可以先回顧一下儲存var宣告的結構體長什麼樣

// NameList Type
    // NameList Type = Values
    // NameList      = Values
    VarDecl struct {
        Group    *Group // nil means not part of a group
        Pragma   Pragma
        NameList []*Name
        Type     Expr // nil means no type
        Values   Expr // nil means no values
        decl
    }

核心欄位就是NameList、Type、Values,我們可以發現在上邊的處理方法中,分別呼叫了三個方法來處理這三個欄位

  1. names := p.declNames(decl.NameList),該方法就是將所有的變數名都轉換成相應的Node結構,Node結構的欄位在前邊已經介紹過了,裡邊核心的欄位就是Op,該方法會給每個Name的Op賦值為ONAME。所以該方法最終返回的是一個Node陣列,裡邊是var宣告的所有變數名
  2. p.typeExprOrNil(decl.Type),該方法是將一個具體的型別轉換成相應的Node結構(比如int、string、slice等,var a int )。該方法主要是呼叫expr(expr syntax.Expr)方法來實現的,它的核心作用就是將指定型別轉換成相應的Node結構(裡邊就是一堆switch case)
  3. p.exprList(decl.Values),該方法就是將值部分轉換成相應的Node結構,核心也是根據值的型別去匹配相應的方法進行解析
  4. variter(names, typ, exprs),它其實就是將var宣告的變數名部分、型別部分、值或表示式部分的Node或Node陣列拼接成一顆樹

前三個方法,就是將var宣告的每部分轉換成相應的Node節點,其實就是設定這個節點的Op屬性,每一個Op就代表一個語義。然後第四個方法就是,根據語義將這些節點拼接成一棵樹,使它能合法的表達var宣告

下邊通過一個示例來展示一下var宣告的抽象語法樹構建

示例展示var宣告的抽象語法樹

假設有下邊這樣一個var宣告的表示式,我先通過標準庫中提供的語法解析方法,展示它解析後的樣子,然後再展示將該結果構建成抽象語法樹之後的樣子

const srcVar = `package test
var a = 666+6
`
func AstVar()  {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "hello.go", srcVar, parser.AllErrors)
    if err != nil {
        log.Fatal(err)
    }
    for _, decl := range f.Decls {
        if v, ok := decl.(*ast.GenDecl); ok {
            fmt.Printf("Tok: %v\n", v.Tok)
            for _, spec := range v.Specs {
                ast.Print(nil, spec)
            }
        }
    }
}

上邊是語法分析之後的結果,然後是呼叫上邊提到的那三個方法,將Names、Type、Values轉換成Node結構如下:

Names:
ONAME(a)

Values:
OLITERAL(666)
OADD(+)
OLITERAL(6)

再通過variter(names, typ, exprs)方法將這些Node構建成一棵樹如下:

你可以通過下邊方式去檢視任何程式碼的語法解析結果:

const src = `你的程式碼`
func Parser()  {
    fset := token.NewFileSet() // positions are relative to fset
    f, err := parser.ParseFile(fset, "", src, 0)
    if err != nil {
        panic(err)
    }

    // Print the AST.
    ast.Print(fset, f)
}

總結

本文先是從整體上認識了一下抽象語法樹做了什麼?以及它的作用是什麼?原始檔中的宣告被構建成抽象語法樹之後長什麼樣子?

然後通過標準庫中提供的語法解析的方法,展示了基礎面值、表示式、Var宣告語句被解析之後的樣子,然後以Var宣告型別為例,展示了抽象語法樹構建階段是如何處理Var宣告語句的

不管是在詞法分析語法分析、抽象語法樹構建階段還是後邊要分享的型別檢查等等,他們的實現必然有很多的細節,這些細節沒法在這裡一一呈現,本系列文章可以幫助小夥伴提供一個清晰的輪廓,大家可以按照這個輪廓去看細節。比如哪些var宣告的使用是合理的、import有哪些寫法,你都可以在Go編譯的底層實現中看到

參考

  • 《編譯原理》
  • 《Go語言底層原理剖析》
  • go-ast-book

相關文章