前言
在上一篇語法分析中,我們知道了Go編譯器是如何按照Go的文法,解析go文字檔案中的各種宣告型別(import、var、const、func等)。語法分析階段將整個原始檔解析到一個File
的結構體中,原始檔中各種宣告型別解析到File.DeclList
中。最終生成以File
結構體為根節點,importDecl
、constDecl
、typeDecl
、varDecl
、FuncDecl等為子節點的語法樹
首先我們需要明確的就是,抽象語法樹的作用其實就是為了後邊進行型別檢查、程式碼風格檢查等等。總之,有了抽象語法樹,編譯器就可以精準的定位到程式碼的任何地方,對其進行一些列的操作及驗證等
本文為抽象語法樹的構建,我們知道,在編譯器前端必須將源程式構建成一種中間表示形式,以便在編譯器的後端進行使用,抽象語法樹就是一種常見的樹狀的中間表示形式。所以本文主要是介紹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,我們可以發現在上邊的處理方法中,分別呼叫了三個方法來處理這三個欄位
- names := p.declNames(decl.NameList),該方法就是將所有的變數名都轉換成相應的Node結構,Node結構的欄位在前邊已經介紹過了,裡邊核心的欄位就是Op,該方法會給每個Name的Op賦值為ONAME。所以該方法最終返回的是一個Node陣列,裡邊是var宣告的所有變數名
- p.typeExprOrNil(decl.Type),該方法是將一個具體的型別轉換成相應的Node結構(比如int、string、slice等,var a int )。該方法主要是呼叫
expr(expr syntax.Expr)方法來實現的,它的核心作用就是將指定型別轉換成相應的Node結構(裡邊就是一堆switch case)
- p.exprList(decl.Values),該方法就是將值部分轉換成相應的Node結構,核心也是根據值的型別去匹配相應的方法進行解析
- 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