以 Golang 為例詳解 AST 抽象語法樹

可可西里發表於2024-01-17

前言

各位同行有沒有想過一件事,一個程式檔案,比如  hello.go 是如何被編譯器理解的,平常在編寫程式時,IDE 又是如何提供程式碼提示的。在這奧妙無窮的背後,  AST(Abstract Syntax Tree)抽象語法樹功不可沒,他站在每一行程式的身後,默默無聞的工作,為繁榮的網際網路世界立下了汗馬功勞。

AST 抽象語法樹

AST 使用樹狀結構來表達程式語言的結構,樹中的每一個節點都表示原始碼中的一個結構。聽到這或許你的心裡會咯噔一下,其實說通俗一點,在原始碼解析後會得到一串資料,這個資料自然的呈現樹狀結構,它被稱之為  CST(Concrete Syntax Tree) 具體語法樹,在 CST 的基礎上保留核心結構。忽略一些不重要的結構,比如標點符號,空白符,括號等,就得到了 AST。

如何生成 AST 

生成 AST 大概需要兩個步驟,詞法分析 lexical analysis和語法分析 syntactic analysis 。

詞法分析 lexical analysis

lexical analysis 簡稱 lexer ,它表示字串序列,也就是我們的原始碼轉化為 token 的過程,進行詞法分析的工具叫做詞法分析器(lexical analyzer,簡稱lexer),也叫掃描器(scanner)。Go 語言的  go/scanner 包提供詞法分析。 

func ScannerDemo() {
	// 原始碼
	src := []byte(`
func demo() {
	fmt.Println("When you are old and gray and full of sleep")
}
`)
	// 初始化標記
	var s scanner.Scanner
	fset := token.NewFileSet()
	file := fset.AddFile("", fset.Base(), len(src))
	s.Init(file, src, nil, scanner.ScanComments)
	// Scan 進行掃碼並列印出結果
	for {
		pos, tok, lit := s.Scan()
		if tok == token.EOF {
			break
		}
		fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
	}
}

列印的結果我們接著往下看。

標記 token

標記(token)  是詞法分析後留下的產物,是構成原始碼的最小單位,但是這些 token 之間沒有任何邏輯關係。以上述程式碼為例:

func demo() {
	fmt.Println("When you are old and gray and full of sleep")
}

經過詞法分析後,會得到:

token literal(字面量,以string表示)
func "func"
IDENT "demo"
( ""
) ""
{ ""
IDENT "fmt"
. ""
IDENT "Println"
( ""
STRING "\"When you are old and gray and full of sleep\""
) ""
; "\n"
} ""
; "\n"

在 Go 語言中,如果 token 型別就是一個字面量,例如整型,字串型別等,那麼它的值就是相對應的值,比如上表的  STRING;如果 token 是 Go 的關鍵詞,那麼它的值就是關鍵詞,比如上表的  fun;對於分號,它的值則是換行符;其他 token 類要麼是不合法的,如果是合法的,則值為空字串,比如上表的  {

語法分析 syntactic analysis

不具備邏輯關係的 token 經過語法分析(syntactic analysis,也叫 parsing)就可以得到具有邏輯關係的 CST 具體語法樹,然後對 CST 進行分析提煉即可得到 AST 抽象語法樹。完成語法分析的工具叫做語法分析器(parser)。Go 語言的  go/parser 提供語法分析。

func ParserDemo() {
	src := `
package main
`
	fset := token.NewFileSet()
	// 如果 src 為 nil,則使用第二個引數,它可以是一個 .go 檔案地址
	f, err := parser.ParseFile(fset, "", src, 0)
	if err != nil {
		panic(err)
	}
	ast.Print(fset, f)
}

列印出來的 AST:

 0  *ast.File {
 1  .  Package: 2:1
 2  .  Name: *ast.Ident {
 3  .  .  NamePos: 2:9
 4  .  .  Name: "main"
 5  .  }
 6  .  FileStart: 1:1
 7  .  FileEnd: 2:14
 8  .  Scope: *ast.Scope {
 9  .  .  Objects: map[string]*ast.Object (len = 0) {}
10  .  }
11  }

它包含了原始碼的結構資訊,看起來像一個 JSON。

總結

原始碼經過 詞法分析後得到 token(標記),token 經過 語法分析得到 CST 具體語法樹,在 CST 上建立 AST 抽象語法樹。 來個圖圖或許更直觀:

以 Golang 為例詳解 AST 抽象語法樹

Go 的抽象語法樹

這裡我們以一個具體的例子來看:從 go 程式碼中提取所有結構體的名稱。

// 原始碼
type A struct{}
type B struct{}
type C struct{}
func ExampleGetStructName() {
	fileSet := token.NewFileSet()
	node, err := parser.ParseFile(fileSet, "demo.go", nil, parser.ParseComments)
	if err != nil {
		return
	}
	ast.Inspect(node, func(n ast.Node) bool {
		if v, ok := n.(*ast.TypeSpec); ok {
			fmt.Println(v.Name.Name)
		}
		return true
	})
	// Output:
	// A
	// B
	// C
}


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70036623/viewspace-3004214/,如需轉載,請註明出處,否則將追究法律責任。

相關文章