Go編譯原理系列4(語法分析)

書旅發表於2022-01-09

前言

在上一篇文章中,分享了Go編譯器是如何將原始檔解析成Token的。本文主要是分享,語法分析階段是如何根據不同的Token來進行語法解析的。本文你可以瞭解到以下內容:

  1. Go語法分析整體概覽
  2. Go語法解析詳細過程
  3. 示例展示語法解析完整過程

? Tips:文中會涉及到文法、產生式相關內容,如果你不太瞭解,請先看一下前邊的詞法分析&語法分析基礎這篇文章

文章比較長,但程式碼比較多,主要是方便理解。相信看完一定有所收穫

Go語法分析整體概覽

為了方便後邊的理解,我這裡提供一個原始檔,後邊的內容,你都可以按照這個原始檔中的內容,帶入去理解:

package main

import (
    "fmt"
    "go/token"
)

type aType string
const A = 666
var B = 888

func main() {
    fmt.Println("Test Parser")
    token.NewFileSet()
}

入口

在上一篇文章介紹詞法分析的時候,詳細的說明了Go的編譯入口,在編譯入口處初始化了語法分析器,並且在初始化語法分析器的過程中初始化了詞法分析器,所以詞法分析器是內嵌在了語法分詞器的內部的,我們可以在:src/cmd/compile/internal/syntax/parser.go中看到語法分析器的結構如下:

type parser struct {
    file  *PosBase //記錄開啟的檔案的資訊的(比如檔名、行、列資訊)
    errh  ErrorHandler //報錯回撥
    mode  Mode //語法分析模式
    pragh PragmaHandler
    scanner //詞法分析器

    base   *PosBase // current position base
    first  error    // first error encountered
    errcnt int      // number of errors encountered
    pragma Pragma   // pragmas

    fnest  int    // function nesting level (for error handling) 函式的巢狀層級
    xnest  int    // expression nesting level (for complit ambiguity resolution) 表示式巢狀級別
    indent []byte // tracing support(跟蹤支援)
}

因為在上一篇文章中已經詳細的分享了入口檔案的位置及它都做了什麼(src/cmd/compile/internal/syntax/syntax.go → Parse(...)),在語法分析器和詞法分析器初始化完成後,我們會看到它呼叫了語法分析器的fileOrNil()方法,它就是語法分析的核心方法,下邊就具體介紹一下這個核心方法具體做了哪些事情

Go語法解析結構

這部分會涉及到每種宣告型別的解析、及每種宣告型別的結構體,還包括語法解析器生成的語法樹的結點之間的關係。有些地方確實比較難理解,你可先大致看一遍文字內容,然後再結合我下邊的那張語法解析的圖,理解起來可能就輕鬆一些

Go語法分析中的文法規則

我先對這部分做整體的介紹,然後再去了解裡邊的細節

進入到fileOrNil()方法,在註釋中會看到這麼一行註釋

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .

它就是Go解析原始檔的文法規則,這個在系列文章的第二篇詞法分析和語法分析基礎部分有提到。Go的編譯器在初始化完語法分析器和詞法分析器之後,就會呼叫該方法,該方法會通過詞法分詞器提供的next()方法,來不斷的獲取Token,語法分析就按照上邊這個文法規則進行語法分析

可能你不知道PackageClause、ImportDecl、TopLevelDecl的產生式,你可以直接在這個檔案中找到這三個產生式(關於產生式,在詞法分析和語法分析基礎部分有介紹)

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .

PackageClause = "package" PackageName . 
PackageName = identifier . 

ImportDecl = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) . 
ImportSpec = [ "." | PackageName ] ImportPath . 
ImportPath = string_lit .

TopLevelDecl = Declaration | FunctionDecl | MethodDecl . 
Declaration = ConstDecl | TypeDecl | VarDecl . 

ConstDecl = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) . 
ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] . 

TypeDecl = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) . 
TypeSpec = AliasDecl | TypeDef . 
AliasDecl = identifier "=" Type . 
TypeDef = identifier Type . 

VarDecl = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) . 
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .

fileOrNil()說白了就是按照SourceFile這個文法來對原始檔進行解析,最終返回的是一個語法樹(File結構,下邊會介紹),我們知道,編譯器會將每一個檔案都解析成一顆語法樹

下邊就簡單的介紹一下Go語法分析中的幾個文法的含義(如果你看了前邊的詞法分析和語法分析基礎這篇文章,應該很容易就能看懂Go這些文法的含義)

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .

我們可以看到SourceFile是由PackageClause、ImportDecl、TopLevelDecl這三個非終結符號構成的。它的意思就是:
每一個原始檔主要是由package宣告、匯入宣告、和頂層宣告構成的(其中ImportDecl和TopLevelDecl是可選的,可有可無,這就是中括號的含義)。也就是說,每一個原始檔,都應該是符合這個文法規則的

首先是包宣告:PackageClause
PackageClause = "package" PackageName . 
PackageName = identifier . 
PackageClause是由一個終結符package和一個非終結符PackageName構成的,而PackageName由一個識別符號構成的

所以,在掃描原始檔的時候,應該會最先獲取到的是package的Token,然後是一個識別符號的Token。解析完package宣告之後,後邊就應該是匯入宣告

然後是匯入宣告:ImportDecl
ImportDecl = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) . 
ImportSpec = [ "." | PackageName ] ImportPath . 
ImportPath = string_lit .

瞭解了上邊這些文法的含義之後,下邊就看fileNil()這個方法是如何按照上邊的文法進行語法解析的。但是在這之前,需要先知道fileOrNil()這個方法生成的語法樹的各個節點結構是什麼樣的

語法樹各個節點的結構

我們知道,fileOrNil()方法會按照SourceFile的文法規則,生成一棵語法樹,而每棵語法樹的結構是這樣的一個結構體:(src/cmd/compile/internal/syntax/nodes.go → File)

type File struct {
    Pragma   Pragma
    PkgName  *Name
    DeclList []Decl
    Lines    uint
    node
}

它主要包含的是原始檔的包名(PkgName)、和原始檔中所有的宣告(DeclList)。需要注意的是,它會將import也當做宣告解析到DeclList中

fileOrNil()會將原始檔中的所有宣告(比如var、type、const)按照每種宣告的結構(每種宣告都定義的有相應的結構體,用來儲存宣告資訊,而這些結構體都是語法樹的子節點)解析到DeclList中。在上邊的文法中,我們能看到常量、型別、變數宣告文法,其實還有函式和方法的文法,可以在src/cmd/compile/internal/syntax/parser.go中找到

FunctionDecl = "func" FunctionName ( Function | Signature ) .
FunctionName = identifier .
Function     = Signature FunctionBody .

MethodDecl   = "func" Receiver MethodName ( Function | Signature ) .
Receiver     = Parameters .

這棵語法樹的根節點結構是File結構體,它的子節點結構就是:

ImportDecl struct {
        Group        *Group // nil means not part of a group
        Pragma       Pragma
        LocalPkgName *Name // including "."; nil means no rename present
        Path         *BasicLit
        decl
    }

ConstDecl 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
    }

TypeDecl struct {
        Group  *Group // nil means not part of a group
        Pragma Pragma
        Name   *Name
        Alias  bool
        Type   Expr
        decl
    }

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
    }

FuncDecl struct {
        Pragma Pragma
        Recv   *Field // nil means regular function
        Name   *Name
        Type   *FuncType
        Body   *BlockStmt // nil means no body (forward declaration)
        decl
    }

這些節點的結構定義在:src/cmd/compile/internal/syntax/nodes.go中。也就是說在解析的過程中,如果解析到滿足ImportDecl文法規則的,它就會建立相應結構的節點來儲存相關資訊;遇到滿足var型別宣告文法的,就建立var型別宣告相關的結構體(VarDecl)來儲存宣告資訊

如果解析到函式就稍微複雜一點,從函式節點的結構可以看到它包含接收者函式名稱型別函式體這幾部分,最複雜的地方在函式體,它是一個BlockStmt的結構:

BlockStmt struct {
        List   []Stmt //Stmt是一個介面
        Rbrace Pos
        stmt
    }

BlockStmt是由一系列的宣告和表示式構成的,你在src/cmd/compile/internal/syntax/nodes.go中可以看到很多表示式和宣告的結構體(這些結構體,也都是函式宣告下邊的節點結構)

// ----------------------------------------------------------------------------
// Statements
......
SendStmt struct {
        Chan, Value Expr // Chan <- Value
        simpleStmt
    }

    DeclStmt struct {
        DeclList []Decl
        stmt
    }

    AssignStmt struct {
        Op       Operator // 0 means no operation
        Lhs, Rhs Expr     // Rhs == ImplicitOne means Lhs++ (Op == Add) or Lhs-- (Op == Sub)
        simpleStmt
    }
......
ReturnStmt struct {
        Results Expr // nil means no explicit return values
        stmt
    }

    IfStmt struct {
        Init SimpleStmt
        Cond Expr
        Then *BlockStmt
        Else Stmt // either nil, *IfStmt, or *BlockStmt
        stmt
    }

    ForStmt struct {
        Init SimpleStmt // incl. *RangeClause
        Cond Expr
        Post SimpleStmt
        Body *BlockStmt
        stmt
    }
......

// ----------------------------------------------------------------------------
// Expressions
......
// [Len]Elem
    ArrayType struct {
        // TODO(gri) consider using Name{"..."} instead of nil (permits attaching of comments)
        Len  Expr // nil means Len is ...
        Elem Expr
        expr
    }

    // []Elem
    SliceType struct {
        Elem Expr
        expr
    }

    // ...Elem
    DotsType struct {
        Elem Expr
        expr
    }

    // struct { FieldList[0] TagList[0]; FieldList[1] TagList[1]; ... }
    StructType struct {
        FieldList []*Field
        TagList   []*BasicLit // i >= len(TagList) || TagList[i] == nil means no tag for field i
        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
    }

    // X.(Type)
    AssertExpr struct {
        X    Expr
        Type Expr
        expr
    }
......

是不是看著很眼熟,有if、for、return這些。BlockStmt中的Stmt是一個介面型別,也就意味著上邊的各種表示式型別或宣告型別結構體都可以實現Stmt介面

知道了語法樹中各個節點之間的關係,以及這些節點都可能會有哪些結構,下邊就看fileOrNil是如何一步一步的解析出這棵語法樹的各個節點的

fileOrNil() 的原始碼實現

這部分我不會深入到每一個函式裡邊去看它是怎麼解析的,只是從大致輪廓上介紹它是怎麼一步一步解析原始檔中的token的,因為這部分主要是先從整體上認識。具體它是怎麼解析import、var、const、func的,我會在下一部分詳細的介紹

可以看到fileOrNil()的程式碼實現主要包含以下幾個部分:

// SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
func (p *parser) fileOrNil() *File {
    ......

  //1.建立File結構體
    f := new(File)
    f.pos = p.pos()

  // 2. 首先解析檔案開頭的package定義
    // PackageClause
    if !p.got(_Package) { //檢查第一行是不是先定義了package
        p.syntaxError("package statement must be first")
        return nil
    }
    
    // 3. 當解析完package之後,解析import宣告(每一個import在解析器看來都是一個宣告語句)
    // { ImportDecl ";" }
    for p.got(_Import) {
        f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
        p.want(_Semi)
    }

    // 4. 根據獲取的token去switch選擇相應的分支,去解析對應型別的語句
    // { TopLevelDecl ";" }
    for p.tok != _EOF {
        switch p.tok {
        case _Const:
            p.next() // 獲取到下一個token
            f.DeclList = p.appendGroup(f.DeclList, p.constDecl)

        ......
    }
    // p.tok == _EOF

    p.clearPragma()
    f.Lines = p.line

    return f
}

在fileOrNil()中有兩個比較重要的方法,是理解fileOrNil()在做的事情的關鍵:

  • got
  • appendGroup

首先是got函式,它的引數是一個token,用來判斷從詞法分析器獲取到的token是不是引數中傳入的這個token

然後是appendGroup函式,它有兩個引數,第一個是DeclList(前邊介紹File的結構體的成員時介紹過,它是用來存放原始檔中所有的宣告的,是一個切片型別);第二個引數是一個函式,這個函式是每種型別宣告語句的分析函式(比如,我當前解析到import宣告語句,那我就將解析import的方法作為第二個引數,傳遞給appendGroup)

在解析import宣告語句的時候,是下邊這麼一段程式碼:

for p.got(_Import) {
        f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
        p.want(_Semi)
}

appendGroup的作用其實就是找出批量的定義,就比如下邊這些情況

//import的批量宣告情況
import (
    "fmt"
    "io"
    "strconv"
    "strings"
)

//var的批量宣告情況
var (
    x int
    y int
)

對於存在批量宣告情況的宣告語句結構體中,它會有一個Group欄位,用來標明這些變數是屬於同一個組,比如import的宣告結構體和var的宣告結構體

ImportDecl struct {
        Group        *Group // nil means not part of a group
        Pragma       Pragma
        LocalPkgName *Name // including "."; nil means no rename present
        Path         *BasicLit
        decl
}

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
}

在appendGroup的方法裡邊,會呼叫相應宣告型別的解析方法,跟fileOrNil()一樣,按照相應型別宣告的文法進行解析。比如對import宣告進行解析的方法importDecl()

for p.got(_Import) {
        f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
        p.want(_Semi)
}
......

// ImportSpec = [ "." | PackageName ] ImportPath .
// ImportPath = string_lit .
func (p *parser) importDecl(group *Group) Decl {
    if trace {
        defer p.trace("importDecl")()
    }

    d := new(ImportDecl)
    d.pos = p.pos()
    d.Group = group
    d.Pragma = p.takePragma()

    switch p.tok {
    case _Name:
        d.LocalPkgName = p.name()
    case _Dot:
        d.LocalPkgName = p.newName(".")
        p.next()
    }
    d.Path = p.oliteral()
    if d.Path == nil {
        p.syntaxError("missing import path")
        p.advance(_Semi, _Rparen)
        return nil
    }

    return d
}

我們可以看見,它也是先建立相應宣告(這裡是import宣告)的結構體,然後記錄宣告的資訊,並按照該種宣告的文法對後邊的內容進行解析

其它的還有像const、type、var、func等宣告語句,通過switch去匹配並解析。他們也有相應的解析方法(其實就是按照各自的文法規則實現的程式碼),我這裡不都列出來,你可以自己在src/cmd/compile/internal/syntax/parser.go中檢視

前邊我們說到,語法分析器最終會使用不通的結構體來構建語法樹的各個節點,其中根節點就是:src/cmd/compile/internal/syntax/nodes.go → File。在前邊已經介紹了它的結構,主要包含包名和所有的宣告型別,這些不同型別的宣告,就是語法樹的子節點

可能通過上邊的文字描述,稍微還是有點難理解,下邊就通過圖的方式來展示整個語法解析的過程是什麼樣的

圖解語法分析過程

以文章開頭提供的原始碼為例來展示語法解析的過程。在Go詞法分析這篇文章中有提到,Go的編譯入口是從src/cmd/compile/internal/gc/noder.go → parseFiles 開始的

到這裡相信你對Go的語法分析部分已經有個整體上的認識了。但是上邊並沒有畫出各種宣告語句是如何往下解析的,這就需要深入的去看每種宣告語句的解析方法是如何實現

Go語法解析詳細過程

關於Go語法分析中各種宣告及表示式的解析,你都可以在src/cmd/compile/internal/syntax/parser.go中找到對應的方法

變數宣告&匯入宣告解析

變數宣告相關解析方法的呼叫,在src/cmd/compile/internal/syntax/parser.go→fileOrNil()方法中都可以找到

匯入宣告解析

在src/cmd/compile/internal/syntax/parser.go→fileOrNil()中可以看到下邊這段程式碼

for p.got(_Import) {
        f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
        p.want(_Semi)
    }

在這個appendGroup中,最終會呼叫importDecl()這個方法(從上邊這段程式碼可以發現,當匹配到import的token之後才會執行匯入宣告的解析方法)

首先需要知道的是,import有以下幾種使用方式:

import "a" //預設的匯入方式,a是包名
import aAlias "x/a" // 給x/a這個包起個別名叫aAlias
import . "c" // 將依賴包的公開符號直接匯入到當前檔案的名字空間
import _ "d" // 只是匯入依賴包觸發其包的初始化動作,但是不匯入任何符號到當前檔名字空間

因為篇幅的原因,我就不把importDecl()方法的原始碼粘出來了。我這裡梳理出來它主要乾了哪些事情:

  1. 建立一個ImportDeal的結構體(最終會被append到File.DeclList中)
  2. 初始化結構體中的一些資料,比如解析到的token的位置資訊、組(group)等
  3. 然後去匹配下一個token,看它是_Name的token型別(識別符號),還是_Dot(.)
  4. 如果獲取到的token是_Name,則獲取到包名,如果獲取到的是_Dot(.),則新建一個名字.
  5. 然後就是匹配包路徑,主要是由oliteral()方法實現
  6. 返回ImportDeal結構

這裡值得說的是oliteral()方法,它會獲取到下一個token,看它是不是一個基礎面值型別的token,也就是_Literal

? Tips:基礎面值只有整數、浮點數、複數、符文和字串幾種型別

如果是基礎面值型別,它就會建立一個基礎面值型別的結構體nodes.BasicLit,並初始化它的一些資訊

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

這就不難看出,當解析到基礎面值型別的時候,它就已經是不可在分解的了,就是文法中說的終結符。在語法樹上,這些終結符都是葉子節點

在go的標準庫中有提供相關的方法來測試一下語法解析,我這裡通過go/parser提供的介面來測一下匯入的時候,語法分析的結果:

? Tips:跟詞法分析一樣(你可以看一下Go詞法分析器這篇文章的標準庫測試部分),標準庫中語法分析的實現和go編譯器中的實現不一樣,主要是結構體的設計(比如跟結點的結構變了,你可以自己去看一下,比較簡單,你明白了編譯器中語法分析的實現,標準庫裡的也能看懂),實現思想是一樣的

package main

import (
    "fmt"
    "go/parser"
    "go/token"
)
const src = `
package test
import "a"
import aAlias "x/a"
import . "c"
import _ "d"
`

func main() {
    fileSet := token.NewFileSet()
    f, err := parser.ParseFile(fileSet, "", src, parser.ImportsOnly) //parser.ImportsOnly模式,表示只解析包宣告和匯入
    if err != nil {
        panic(err.Error())
    }
    for _, s := range f.Imports{
        fmt.Printf("import:name = %v, path = %#v\n", s.Name, s.Path)
    }
}

列印結果:

import:name = <nil>, path = &ast.BasicLit{ValuePos:22, Kind:9, Value:"\"a\""}
import:name = aAlias, path = &ast.BasicLit{ValuePos:40, Kind:9, Value:"\"x/a\""}
import:name = ., path = &ast.BasicLit{ValuePos:55, Kind:9, Value:"\"c\""}
import:name = _, path = &ast.BasicLit{ValuePos:68, Kind:9, Value:"\"d\""}

後邊的各種型別宣告解析或表示式的解析,你都可以通過標準庫裡邊提供的方法來進行測試,下邊的我就不一一的展示測試了(測試方法一樣,只是你需要列印的欄位變一下就行了)

type型別宣告解析

當語法分析器獲取到_Type這個token的時候,它就會呼叫type的解析方法去解析,同樣在fileOrNil()中,你可以看到如下程式碼:

......
case _Type:
            p.next()
            f.DeclList = p.appendGroup(f.DeclList, p.typeDecl)
......

它會在appendGroup中呼叫typeDecl()方法,該方法就是按照type型別宣告的文法去進行語法解析,這個在前邊已經介紹了。我們知道type有以下用法:

type a string
type b = string

下邊看這個方法中具體做了哪些事情:

  • 建立一個TypeDecl的結構體(最終會被append到File.DeclList中)
  • 初始化結構體中的一些資料,比如解析到的token的位置資訊、組(group)等
  • 下一個token是否是_Assign,如果是,則獲取下一個token
  • 驗證下一個token的型別,比如是chan、struct、map還是func等(在typeOrNil()方法中實現的,其實就是一堆switch case)
  • 返回TypeDecl結構

在獲取最右邊那個token的型別的時候,需要根據它的型別,繼續往下解析。假設是一個chan型別,那就會建立一個chan型別的結構體,初始化這個chan型別的資訊(在解析過程中,建立的每一種型別的結構體,都是一個結點)

你同樣可以用go/parser→ParseFile來測試type的語法分析

const型別宣告解析

當語法分析器獲取到_Const這個token的時候,它就會呼叫const的解析方法去解析,同樣在fileOrNil()中,你可以看到如下程式碼:

......
case _Const:
            p.next()
            f.DeclList = p.appendGroup(f.DeclList, p.constDecl)
......

const有以下用法:

const A = 666
const B float64 = 6.66
const (
    _   token = iota
    _EOF       
    _Name    
    _Literal
)

然後就可以去看constDecl方法中的具體實現(其實按照const宣告的文法進行解析)。這裡就不再重複了,跟上邊type的解析差不多,都是先建立相應結構體,然後記錄該型別的一些資訊。它不一樣的地方就是,它有個名字列表,因為const可以同時宣告多個。解析方法是parser.go→nameList()

var型別宣告解析

當語法分析器獲取到_Var這個token的時候,它就會呼叫var的解析方法去解析,同樣在fileOrNil()中,你可以看到如下程式碼:

......
case _Var:
            p.next()
            f.DeclList = p.appendGroup(f.DeclList, p.varDecl)
......

跟上邊的兩種宣告一樣,會呼叫相應的解析方法。var不同的是,它的宣告裡邊,可能涉及表示式,所以在var的解析方法中涉及到表示式的解析,我會在後邊部分詳細分析表示式的解析

函式宣告解析實現

? 說明:後邊會加個圖來展示函式解析的過程

最後就是函式宣告的解析。前邊已經提到,函式宣告節點的結構如下:

FuncDecl struct {
        Pragma Pragma
        Recv   *Field // 接收者
        Name   *Name  //函式名
        Type   *FuncType //函式型別
        Body   *BlockStmt // 函式體
        decl
    }

在fileOrNil()中,你可以看到如下程式碼:

case _Func:
            p.next()
            if d := p.funcDeclOrNil(); d != nil {
                f.DeclList = append(f.DeclList, d)
            }

解析函式的核心方法就是funcDeclOrNil,因為函式的解析稍微複雜點,我這裡把它的實現粘出來,通過註釋來說明每行程式碼在做什麼

// 函式的文法
// FunctionDecl = "func" FunctionName ( Function | Signature ) .
// FunctionName = identifier .
// Function     = Signature FunctionBody .

// 方法的文法
// MethodDecl   = "func" Receiver MethodName ( Function | Signature ) .
// Receiver     = Parameters . //方法的接收者
func (p *parser) funcDeclOrNil() *FuncDecl {
    if trace {
        defer p.trace("funcDecl")()
    }

    f := new(FuncDecl) //建立函式宣告型別的結構體(節點)
    f.pos = p.pos()
    f.Pragma = p.takePragma()

    if p.tok == _Lparen { //如果匹配到了左小括號(說明是方法)
        rcvr := p.paramList() //獲取接收者列表
        switch len(rcvr) {
        case 0:
            p.error("method has no receiver")
        default:
            p.error("method has multiple receivers")
            fallthrough
        case 1:
            f.Recv = rcvr[0]
        }
    }

    if p.tok != _Name { //判斷下一個token是否是識別符號(即函式名)
        p.syntaxError("expecting name or (")
        p.advance(_Lbrace, _Semi)
        return nil
    }

    f.Name = p.name()
    f.Type = p.funcType() //獲取型別(下邊繼續瞭解其內部實現)
    if p.tok == _Lbrace { // 如果匹配到左中括號,則開始解析函式體
        f.Body = p.funcBody() //解析函式體(下邊繼續瞭解其內部實現)
    }

    return f
}

函式解析部分比較重要的兩個實現:funcType()funcBody()。具體看他們內部做了什麼?

/*
FuncType struct {
        ParamList  []*Field
        ResultList []*Field
        expr
}
*/
func (p *parser) funcType() *FuncType {
    if trace {
        defer p.trace("funcType")()
    }

    typ := new(FuncType) //建立函式型別結構體(主要成員是引數列表和返回值列表)
    typ.pos = p.pos()
    typ.ParamList = p.paramList() //獲取引數列表(它返回的是一個Field結構體,它的成員是引數名和型別)
    typ.ResultList = p.funcResult() //獲取返回值列表(它返回的也是一個Field結構體)

    return typ
}
func (p *parser) funcBody() *BlockStmt {
    p.fnest++ // 記錄函式的呼叫層級
    errcnt := p.errcnt // 記錄當前的錯誤數
    body := p.blockStmt("") // 解析函式體中的語句(具體實現繼續往下看)
    p.fnest--

    // Don't check branches if there were syntax errors in the function
    // as it may lead to spurious errors (e.g., see test/switch2.go) or
    // possibly crashes due to incomplete syntax trees.
    if p.mode&CheckBranches != 0 && errcnt == p.errcnt {
        checkBranches(body, p.errh)
    }

    return body
}

func (p *parser) blockStmt(context string) *BlockStmt {
    if trace {
        defer p.trace("blockStmt")()
    }

    s := new(BlockStmt) //建立函式體的結構
    s.pos = p.pos()

    // people coming from C may forget that braces are mandatory in Go
    if !p.got(_Lbrace) {
        p.syntaxError("expecting { after " + context)
        p.advance(_Name, _Rbrace)
        s.Rbrace = p.pos() // in case we found "}"
        if p.got(_Rbrace) {
            return s
        }
    }

    s.List = p.stmtList() //開始解析函式體中的宣告及表示式(這裡邊的實現就是根據獲取的token來判斷是哪種宣告或語句,也是通過switch case來實現,根據匹配的型別進行相應文法的解析)
    s.Rbrace = p.pos()
    p.want(_Rbrace)

    return s
}

上邊函式解析過程用圖來展示一下,方便理解:

關於在函式體中的一些像賦值、for、go、defer、select等等是如何解析,可以自行去看

總結

本文主要是從整體上分享了語法解析的過程,並且簡單的展示了type、const、func宣告解析的內部實現。其實語法解析裡邊還有表示式解析、包括其他的一些語法的解析,內容比較多,沒法一一介紹,感興趣的小夥伴可以自行研究

感謝閱讀,下一篇主題為:抽象語法樹構建

相關文章