日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

(四)Go 语言编译流程简述

發(fā)布時(shí)間:2025/3/15 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 (四)Go 语言编译流程简述 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

一、概述

Go 語(yǔ)言編譯的最后一個(gè)階段是根據(jù) SSA 中間代碼生成機(jī)器碼,這里談的機(jī)器碼是在目標(biāo) CPU 架構(gòu)上能夠運(yùn)行的二進(jìn)制代碼,中間代碼生成一節(jié)簡(jiǎn)單介紹的從抽象語(yǔ)法樹到 SSA 中間代碼的生成過(guò)程,將近 50 個(gè)生成中間代碼的步驟中有一些過(guò)程嚴(yán)格上說(shuō)是屬于機(jī)器碼生成階段的。

機(jī)器碼的生成過(guò)程其實(shí)是對(duì) SSA 中間代碼的降級(jí)(lower)過(guò)程,在 SSA 中間代碼降級(jí)的過(guò)程中,編譯器將一些值重寫成了目標(biāo) CPU 架構(gòu)的特定值,降級(jí)的過(guò)程處理了所有機(jī)器特定的重寫規(guī)則并對(duì)代碼進(jìn)行了一定程度的優(yōu)化;在 SSA 中間代碼生成階段的最后,Go 函數(shù)體的代碼會(huì)被轉(zhuǎn)換成?cmd/compile/internal/obj.Prog?結(jié)構(gòu)。

指令集架構(gòu)?#

首先需要介紹的就是指令集架構(gòu),雖然我們?cè)诘谝还?jié)編譯過(guò)程概述中曾經(jīng)講解過(guò)指令集架構(gòu),但是在這里還是需要引入更多的指令集架構(gòu)知識(shí)。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖??計(jì)算機(jī)軟硬件之間的橋梁

指令集架構(gòu)是計(jì)算機(jī)的抽象模型,在很多時(shí)候也被稱作架構(gòu)或者計(jì)算機(jī)架構(gòu),它是計(jì)算機(jī)軟件和硬件之間的接口和橋梁1;一個(gè)為特定指令集架構(gòu)編寫的應(yīng)用程序能夠運(yùn)行在所有支持這種指令集架構(gòu)的機(jī)器上,也就是說(shuō)如果當(dāng)前應(yīng)用程序支持 x86 的指令集,那么就可以運(yùn)行在所有使用 x86 指令集的機(jī)器上,這其實(shí)就是抽象層的作用,每一個(gè)指令集架構(gòu)都定義了支持的數(shù)據(jù)結(jié)構(gòu)、寄存器、管理主內(nèi)存的硬件支持(例如內(nèi)存一致、地址模型和虛擬內(nèi)存)、支持的指令集和 IO 模型,它的引入其實(shí)就在軟件和硬件之間引入了一個(gè)抽象層,讓同一個(gè)二進(jìn)制文件能夠在不同版本的硬件上運(yùn)行。

如果一個(gè)編程語(yǔ)言想要在所有的機(jī)器上運(yùn)行,它就可以將中間代碼轉(zhuǎn)換成使用不同指令集架構(gòu)的機(jī)器碼,這可比為不同硬件單獨(dú)移植要簡(jiǎn)單的太多了。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖? 復(fù)雜指令集(CISC)和精簡(jiǎn)指令集(RISC)

最常見的指令集架構(gòu)分類方法是根據(jù)指令的復(fù)雜度將其分為復(fù)雜指令集(CISC)和精簡(jiǎn)指令集(RISC),復(fù)雜指令集架構(gòu)包含了很多特定的指令,但是其中的一些指令很少會(huì)被程序使用,而精簡(jiǎn)指令集只實(shí)現(xiàn)了經(jīng)常被使用的指令,不常用的操作都會(huì)通過(guò)組合簡(jiǎn)單指令來(lái)實(shí)現(xiàn)。

復(fù)雜指令集的特點(diǎn)就是指令數(shù)目多并且復(fù)雜,每條指令的字節(jié)長(zhǎng)度并不相等,x86 就是常見的復(fù)雜指令集處理器,它的指令長(zhǎng)度大小范圍非常廣,從 1 到 15 字節(jié)不等,對(duì)于長(zhǎng)度不固定的指令,計(jì)算機(jī)必須額外對(duì)指令進(jìn)行判斷,這需要付出額外的性能損失2。

而精簡(jiǎn)指令集對(duì)指令的數(shù)目和尋址方式做了精簡(jiǎn),大大減少指令數(shù)量的同時(shí)更容易實(shí)現(xiàn),指令集中的每一個(gè)指令都使用標(biāo)準(zhǔn)的字節(jié)長(zhǎng)度、執(zhí)行時(shí)間相比復(fù)雜指令集會(huì)少很多,處理器在處理指令時(shí)也可以流水執(zhí)行,提高了對(duì)并行的支持。作為一種常見的精簡(jiǎn)指令集處理器,arm 使用 4 個(gè)字節(jié)作為指令的固定長(zhǎng)度,省略了判斷指令的性能損失3,精簡(jiǎn)指令集其實(shí)就是利用了我們耳熟能詳?shù)?20/80 原則,用 20% 的基礎(chǔ)指令和它們的組合來(lái)解決問(wèn)題。

最開始的計(jì)算機(jī)使用復(fù)雜指令集是因?yàn)楫?dāng)時(shí)計(jì)算機(jī)的性能和內(nèi)存比較有限,業(yè)界需要盡可能地減少機(jī)器需要執(zhí)行的指令,所以更傾向于高度編碼、長(zhǎng)度不等以及多操作數(shù)的指令。不過(guò)隨著計(jì)算機(jī)性能的提升,出現(xiàn)了精簡(jiǎn)指令集這種犧牲代碼密度換取簡(jiǎn)單實(shí)現(xiàn)的設(shè)計(jì);除此之外,硬件的飛速提升還帶來(lái)了更多的寄存器和更高的時(shí)鐘頻率,軟件開發(fā)人員也不再直接接觸匯編代碼,而是通過(guò)編譯器和匯編器生成指令,復(fù)雜的機(jī)器指令對(duì)于編譯器來(lái)說(shuō)很難利用,所以精簡(jiǎn)指令在這種場(chǎng)景下更適合。

復(fù)雜指令集和精簡(jiǎn)指令集的使用是設(shè)計(jì)上的權(quán)衡,經(jīng)過(guò)這么多年的發(fā)展,兩種指令集也相互借鑒和學(xué)習(xí),與最開始剛被設(shè)計(jì)出來(lái)時(shí)已經(jīng)有了較大的差別,對(duì)于軟件工程師來(lái)講,復(fù)雜的硬件設(shè)備對(duì)于我們來(lái)說(shuō)已經(jīng)是領(lǐng)域下三層的知識(shí)了,其實(shí)不太需要掌握太多,但是對(duì)指令集架構(gòu)感興趣的讀者可以找一些資料開拓眼界。

機(jī)器碼生成?#

機(jī)器碼的生成在 Go 的編譯器中主要由兩部分協(xié)同工作,其中一部分是負(fù)責(zé) SSA 中間代碼降級(jí)和根據(jù)目標(biāo)架構(gòu)進(jìn)行特定處理的?cmd/compile/internal/ssa?包,另一部分是負(fù)責(zé)生成機(jī)器碼的?cmd/internal/obj4:

  • cmd/compile/internal/ssa?主要負(fù)責(zé)對(duì) SSA 中間代碼進(jìn)行降級(jí)、執(zhí)行架構(gòu)特定的優(yōu)化和重寫并生成?cmd/compile/internal/obj.Prog?指令;
  • cmd/internal/obj?作為匯編器會(huì)將這些指令轉(zhuǎn)換成機(jī)器碼完成這次編譯;

SSA 降級(jí)?#

SSA 降級(jí)是在中間代碼生成的過(guò)程中完成的,其中將近 50 輪處理的過(guò)程中,lower?以及后面的階段都屬于 SSA 降級(jí)這一過(guò)程,這么多輪的處理會(huì)將 SSA 轉(zhuǎn)換成機(jī)器特定的操作:

var passes = [...]pass{...{name: "lower", fn: lower, required: true},{name: "lowered deadcode for cse", fn: deadcode}, // deadcode immediately before CSE avoids CSE making dead values live again{name: "lowered cse", fn: cse},...{name: "trim", fn: trim}, // remove empty blocks }

SSA 降級(jí)執(zhí)行的第一個(gè)階段就是?lower,該階段的入口方法是?cmd/compile/internal/ssa.lower?函數(shù),它會(huì)將 SSA 的中間代碼轉(zhuǎn)換成機(jī)器特定的指令:

func lower(f *Func) {applyRewrite(f, f.Config.lowerBlock, f.Config.lowerValue) }

向?cmd/compile/internal/ssa.applyRewrite?傳入的兩個(gè)函數(shù)?lowerBlock?和?lowerValue?是在中間代碼生成階段初始化 SSA 配置時(shí)確定的,這兩個(gè)函數(shù)會(huì)分別轉(zhuǎn)換函數(shù)中的代碼塊和代碼塊中的值。

假設(shè)目標(biāo)機(jī)器使用 x86 的架構(gòu),最終會(huì)調(diào)用?cmd/compile/internal/ssa.rewriteBlock386?和?cmd/compile/internal/ssa.rewriteValue386?兩個(gè)函數(shù),這兩個(gè)函數(shù)是兩個(gè)巨大的 switch 語(yǔ)句,前者總共有 2000 多行,后者將近 700 行,用于處理 x86 架構(gòu)重寫的函數(shù)總共有將近 30000 行代碼,你能在?cmd/compile/internal/ssa/rewrite386.go?這里找到文件的全部?jī)?nèi)容,我們只節(jié)選其中的一段展示一下:

func rewriteValue386(v *Value) bool {switch v.Op {case Op386ADCL:return rewriteValue386_Op386ADCL_0(v)case Op386ADDL:return rewriteValue386_Op386ADDL_0(v) || rewriteValue386_Op386ADDL_10(v) || rewriteValue386_Op386ADDL_20(v)...} }func rewriteValue386_Op386ADCL_0(v *Value) bool {// match: (ADCL x (MOVLconst [c]) f)// cond:// result: (ADCLconst [c] x f)for {_ = v.Args[2]x := v.Args[0]v_1 := v.Args[1]if v_1.Op != Op386MOVLconst {break}c := v_1.AuxIntf := v.Args[2]v.reset(Op386ADCLconst)v.AuxInt = cv.AddArg(x)v.AddArg(f)return true}... }

重寫的過(guò)程會(huì)將通用的 SSA 中間代碼轉(zhuǎn)換成目標(biāo)架構(gòu)特定的指令,上述的?rewriteValue386_Op386ADCL_0?函數(shù)會(huì)使用?ADCLconst?替換?ADCL?和?MOVLconst?兩條指令,它能通過(guò)對(duì)指令的壓縮和優(yōu)化減少在目標(biāo)硬件上執(zhí)行所需要的時(shí)間和資源。

我們?cè)谏弦还?jié)中間代碼生成中已經(jīng)介紹過(guò)?cmd/compile/internal/gc.compileSSA?中調(diào)用?cmd/compile/internal/gc.buildssa?的執(zhí)行過(guò)程,我們?cè)谶@里繼續(xù)介紹?cmd/compile/internal/gc.buildssa?函數(shù)返回后的邏輯:

func compileSSA(fn *Node, worker int) {f := buildssa(fn, worker)pp := newProgs(fn, worker)defer pp.Free()genssa(f, pp)pp.Flush() }

cmd/compile/internal/gc.genssa?函數(shù)會(huì)創(chuàng)建一個(gè)新的?cmd/compile/internal/gc.Progs?結(jié)構(gòu)并將生成的 SSA 中間代碼都存入新建的結(jié)構(gòu)體中,我們?cè)谏弦还?jié)得到的 ssa.html 文件就包含最后生成的中間代碼:

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 genssa 的執(zhí)行結(jié)果

上述輸出結(jié)果跟最后生成的匯編代碼已經(jīng)非常相似了,隨后調(diào)用的?cmd/compile/internal/gc.Progs.Flush?會(huì)使用?cmd/internal/obj?包中的匯編器將 SSA 轉(zhuǎn)換成匯編代碼:

func (pp *Progs) Flush() {plist := &obj.Plist{Firstpc: pp.Text, Curfn: pp.curfn}obj.Flushplist(Ctxt, plist, pp.NewProg, myimportpath) }

cmd/compile/internal/gc.buildssa?中的?lower?和隨后的多個(gè)階段會(huì)對(duì) SSA 進(jìn)行轉(zhuǎn)換、檢查和優(yōu)化,生成機(jī)器特定的中間代碼,接下來(lái)通過(guò)?cmd/compile/internal/gc.genssa?將代碼輸出到?cmd/compile/internal/gc.Progs?對(duì)象中,這也是代碼進(jìn)入?yún)R編器前的最后一個(gè)步驟。

匯編器?#

匯編器是將匯編語(yǔ)言翻譯為機(jī)器語(yǔ)言的程序,Go 語(yǔ)言的匯編器是基于 Plan 9 匯編器的輸入類型設(shè)計(jì)的,Go 語(yǔ)言對(duì)于匯編語(yǔ)言 Plan 9 和匯編器的資料十分缺乏,網(wǎng)上能夠找到的資料也大多都含糊不清,官方對(duì)匯編器在不同處理器架構(gòu)上的實(shí)現(xiàn)細(xì)節(jié)也沒(méi)有明確定義:

The details vary with architecture, and we apologize for the imprecision; the situation is?not well-defined.5

我們?cè)谘芯繀R編器和匯編語(yǔ)言時(shí)不應(yīng)該陷入細(xì)節(jié),只需要理解匯編語(yǔ)言的執(zhí)行邏輯就能夠幫助我們快速讀懂匯編代碼。當(dāng)我們將如下的代碼編譯成匯編指令時(shí),會(huì)得到如下的內(nèi)容:

$ cat hello.go package hellofunc hello(a int) int {c := a + 2return c } $ GOOS=linux GOARCH=amd64 go tool compile -S hello.go "".hello STEXT nosplit size=15 args=0x10 locals=0x00x0000 00000 (main.go:3) TEXT "".hello(SB), NOSPLIT, $0-160x0000 00000 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (main.go:3) FUNCDATA $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0000 00000 (main.go:4) PCDATA $2, $00x0000 00000 (main.go:4) PCDATA $0, $00x0000 00000 (main.go:4) MOVQ "".a+8(SP), AX0x0005 00005 (main.go:4) ADDQ $2, AX0x0009 00009 (main.go:5) MOVQ AX, "".~r1+16(SP)0x000e 00014 (main.go:5) RET0x0000 48 8b 44 24 08 48 83 c0 02 48 89 44 24 10 c3 H.D$.H...H.D$.. ...

上述匯編代碼都是由?cmd/internal/obj.Flushplist?這個(gè)函數(shù)生成的,該函數(shù)會(huì)調(diào)用架構(gòu)特定的?Preprocess?和?Assemble?方法:

func Flushplist(ctxt *Link, plist *Plist, newprog ProgAlloc, myimportpath string) {...for _, s := range text {mkfwd(s)linkpatch(ctxt, s, newprog)ctxt.Arch.Preprocess(ctxt, s, newprog)ctxt.Arch.Assemble(ctxt, s, newprog)linkpcln(ctxt, s)ctxt.populateDWARF(plist.Curfn, s, myimportpath)} }

Go 編譯器會(huì)在最外層的主函數(shù)確定調(diào)用的?Preprocess?和?Assemble?方法,編譯器在 2.1.4 中提到的?cmd/compile.archInits?中根據(jù)目標(biāo)硬件初始化當(dāng)前架構(gòu)使用的配置。

如果目標(biāo)機(jī)器的架構(gòu)是 x86,那么這兩個(gè)函數(shù)最終會(huì)使用?cmd/internal/obj/x86.preprocess?和?cmd/internal/obj/x86.span6,作者在這里就不展開介紹這兩個(gè)特別復(fù)雜的底層函數(shù)了,有興趣的讀者可以通過(guò)鏈接找到目標(biāo)函數(shù)的位置了解預(yù)處理和匯編的處理過(guò)程,機(jī)器碼的生成也都是由這兩個(gè)函數(shù)組合完成的。

小結(jié)?#

機(jī)器碼生成作為 Go 語(yǔ)言編譯的最后一步,其實(shí)已經(jīng)到了硬件和機(jī)器指令這一層,其中對(duì)于內(nèi)存、寄存器的處理非常復(fù)雜并且難以閱讀,想要真正掌握這里的處理的步驟和原理還是需要耗費(fèi)很多精力。

作為軟件工程師,如果不是 Go 語(yǔ)言編譯器的開發(fā)者或者需要經(jīng)常處理匯編語(yǔ)言和機(jī)器指令,掌握這些知識(shí)的投資回報(bào)率實(shí)在太低,我們只需要對(duì)這個(gè)過(guò)程有所了解,補(bǔ)全知識(shí)上的盲點(diǎn),在遇到問(wèn)題時(shí)能夠快速定位即可。

延伸閱讀?#

  • A Manual for the Plan 9 assembler


  • Instruction set architecture?https://en.wikipedia.org/wiki/Instruction_set_architecture???

  • 復(fù)雜指令集 Complex instruction set computer?https://en.wikipedia.org/wiki/Complex_instruction_set_computer???

  • 精簡(jiǎn)指令集 Reduced instruction set computer?https://en.wikipedia.org/wiki/Reduced_instruction_set_computer???

  • Introduction to the Go compiler?go/README.md at master · golang/go · GitHub???

  • A Quick Guide to Go’s Assembler?https://golang.org/doc/asm???

  • 二、詞法和語(yǔ)法分析

    當(dāng)使用通用編程語(yǔ)言1進(jìn)行編寫代碼時(shí),我們一定要認(rèn)識(shí)到代碼首先是寫給人看的,只是恰好可以被機(jī)器編譯和執(zhí)行,而很難被人理解和維護(hù)的代碼是非常糟糕。代碼其實(shí)是按照約定格式編寫的字符串,經(jīng)過(guò)訓(xùn)練的軟件工程師能對(duì)本來(lái)無(wú)意義的字符串進(jìn)行分組和分析,按照約定的語(yǔ)法來(lái)理解源代碼,并在腦內(nèi)編譯并運(yùn)行程序。

    既然工程師能夠按照一定的方式理解和編譯 Go 語(yǔ)言的源代碼,那么我們?nèi)绾文M人理解源代碼的方式構(gòu)建一個(gè)能夠分析編程語(yǔ)言代碼的程序呢。我們?cè)谶@一節(jié)中將介紹詞法分析和語(yǔ)法分析這兩個(gè)重要的編譯過(guò)程,這兩個(gè)過(guò)程能將原本機(jī)器看來(lái)無(wú)序意義的源文件轉(zhuǎn)換成更容易理解、分析并且結(jié)構(gòu)化的抽象語(yǔ)法樹,接下來(lái)我們就看一看解析器眼中的 Go 語(yǔ)言是什么樣的。

    2.2.1 詞法分析?#

    源代碼在計(jì)算機(jī)『眼中』其實(shí)是一團(tuán)亂麻,一個(gè)由字符組成的、無(wú)法被理解的字符串,所有的字符在計(jì)算器看來(lái)并沒(méi)有什么區(qū)別,為了理解這些字符我們需要做的第一件事情就是將字符串分組,這能夠降低理解字符串的成本,簡(jiǎn)化源代碼的分析過(guò)程。

    make(chan int)

    哪怕是不懂編程的人看到上述文本的第一反應(yīng)也應(yīng)該會(huì)將上述字符串分成幾個(gè)部分 -?make、chan、int?和括號(hào),這個(gè)憑直覺(jué)分解文本的過(guò)程就是詞法分析,詞法分析是將字符序列轉(zhuǎn)換為標(biāo)記(token)序列的過(guò)程2。

    lex?#

    lex3?是用于生成詞法分析器的工具,lex 生成的代碼能夠?qū)⒁粋€(gè)文件中的字符分解成 Token 序列,很多語(yǔ)言在設(shè)計(jì)早期都會(huì)使用它快速設(shè)計(jì)出原型。詞法分析作為具有固定模式的任務(wù),出現(xiàn)這種更抽象的工具必然的,lex 作為一個(gè)代碼生成器,使用了類似 C 語(yǔ)言的語(yǔ)法,我們將 lex 理解為正則匹配的生成器,它會(huì)使用正則匹配掃描輸入的字符流,下面是一個(gè) lex 文件的示例:

    %{ #include <stdio.h> %}%% package printf("PACKAGE "); import printf("IMPORT "); \. printf("DOT "); \{ printf("LBRACE "); \} printf("RBRACE "); \( printf("LPAREN "); \) printf("RPAREN "); \" printf("QUOTE "); \n printf("\n"); [0-9]+ printf("NUMBER "); [a-zA-Z_]+ printf("IDENT "); %%

    這個(gè)定義好的文件能夠解析?package?和?import?關(guān)鍵字、常見的特殊字符、數(shù)字以及標(biāo)識(shí)符,雖然這里的規(guī)則可能有一些簡(jiǎn)陋和不完善,但是用來(lái)解析下面的這一段代碼還是比較輕松的:

    package mainimport ("fmt" )func main() {fmt.Println("Hello") }

    .l?結(jié)尾的 lex 代碼并不能直接運(yùn)行,我們首先需要通過(guò)?lex?命令將上面的?simplego.l?展開成 C 語(yǔ)言代碼,這里可以直接執(zhí)行如下所示的命令編譯并打印文件中的內(nèi)容:

    $ lex simplego.l $ cat lex.yy.c ... int yylex (void) {...while ( 1 ) {... yy_match:do {register YY_CHAR yy_c = yy_ec[YY_SC_TO_UI(*yy_cp)];if ( yy_accept[yy_current_state] ) {(yy_last_accepting_state) = yy_current_state;(yy_last_accepting_cpos) = yy_cp;}while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state ) {yy_current_state = (int) yy_def[yy_current_state];if ( yy_current_state >= 30 )yy_c = yy_meta[(unsigned int) yy_c];}yy_current_state = yy_nxt[yy_base[yy_current_state] + (unsigned int) yy_c];++yy_cp;} while ( yy_base[yy_current_state] != 37 );...do_action:switch ( yy_act )case 0:...case 1:YY_RULE_SETUPprintf("PACKAGE ");YY_BREAK... }

    lex.yy.c4?的前 600 行基本都是宏和函數(shù)的聲明和定義,后面生成的代碼大都是為?yylex?這個(gè)函數(shù)服務(wù)的,這個(gè)函數(shù)使用有限自動(dòng)機(jī)(Deterministic Finite Automaton、DFA)5的程序結(jié)構(gòu)來(lái)分析輸入的字符流,上述代碼中?while?循環(huán)就是這個(gè)有限自動(dòng)機(jī)的主體,你如果仔細(xì)看這個(gè)文件生成的代碼會(huì)發(fā)現(xiàn)當(dāng)前的文件中并不存在?main?函數(shù),main?函數(shù)是在 liblex 庫(kù)中定義的,所以在編譯時(shí)其實(shí)需要添加額外的?-ll?選項(xiàng):

    $ cc lex.yy.c -o simplego -ll $ cat main.go | ./simplego

    當(dāng)我們將 C 語(yǔ)言代碼通過(guò) gcc 編譯成二進(jìn)制代碼之后,就可以使用管道將上面提到的 Go 語(yǔ)言代碼作為輸入傳遞到生成的詞法分析器中,這個(gè)詞法分析器會(huì)打印出如下的內(nèi)容:

    PACKAGE IDENTIMPORT LPARENQUOTE IDENT QUOTE RPARENIDENT IDENT LPAREN RPAREN LBRACEIDENT DOT IDENT LPAREN QUOTE IDENT QUOTE RPAREN RBRACE

    從上面的輸出我們能夠看到 Go 源代碼的影子,lex 生成的詞法分析器 lexer 通過(guò)正則匹配的方式將機(jī)器原本很難理解的字符串進(jìn)行分解成很多的 Token,有利于后面的處理。

    圖 ?從 .l 文件到二進(jìn)制

    到這里我們已經(jīng)為各位讀者展示了從定義?.l?文件、使用 lex 將?.l?文件編譯成 C 語(yǔ)言代碼以及二進(jìn)制的全過(guò)程,而最后生成的詞法分析器也能夠?qū)⒑?jiǎn)單的 Go 語(yǔ)言代碼進(jìn)行轉(zhuǎn)換成 Token 序列。lex 的使用還是比較簡(jiǎn)單的,我們可以使用它快速實(shí)現(xiàn)詞法分析器,相信各位讀者對(duì)它也有了一定的了解。

    Go?#

    Go 語(yǔ)言的詞法解析是通過(guò)?src/cmd/compile/internal/syntax/scanner.go6?文件中的?cmd/compile/internal/syntax.scanner?結(jié)構(gòu)體實(shí)現(xiàn)的,這個(gè)結(jié)構(gòu)體會(huì)持有當(dāng)前掃描的數(shù)據(jù)源文件、啟用的模式和當(dāng)前被掃描到的 Token:

    type scanner struct {sourcemode uintnlsemi bool// current token, valid after calling next()line, col uintblank bool // line is blank up to coltok tokenlit string // valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF"); may be malformed if bad is truebad bool // valid if tok is _Literal, true if a syntax error occurred, lit may be malformedkind LitKind // valid if tok is _Literalop Operator // valid if tok is _Operator, _AssignOp, or _IncOpprec int // valid if tok is _Operator, _AssignOp, or _IncOp }

    src/cmd/compile/internal/syntax/tokens.go7?文件中定義了 Go 語(yǔ)言中支持的全部 Token 類型,所有的?token?類型都是正整數(shù),你可以在這個(gè)文件中找到一些常見 Token 的定義,例如:操作符、括號(hào)和關(guān)鍵字等:

    const (_ token = iota_EOF // EOF// operators and operations_Operator // op...// delimiters_Lparen // (_Lbrack // [...// keywords_Break // break..._Type // type_Var // vartokenCount // )

    從 Go 語(yǔ)言中定義的 Token 類型,我們可以將語(yǔ)言中的元素分成幾個(gè)不同的類別,分別是名稱和字面量、操作符、分隔符和關(guān)鍵字。詞法分析主要是由?cmd/compile/internal/syntax.scanner?這個(gè)結(jié)構(gòu)體中的?cmd/compile/internal/syntax.scanner.next?方法驅(qū)動(dòng),這個(gè) 250 行函數(shù)的主體是一個(gè) switch/case 結(jié)構(gòu):

    func (s *scanner) next() {...s.stop()startLine, startCol := s.pos()for s.ch == ' ' || s.ch == '\t' || s.ch == '\n' && !nlsemi || s.ch == '\r' {s.nextch()}s.line, s.col = s.pos()s.blank = s.line > startLine || startCol == colbases.start()if isLetter(s.ch) || s.ch >= utf8.RuneSelf && s.atIdentChar(true) {s.nextch()s.ident()return}switch s.ch {case -1:s.tok = _EOFcase '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':s.number(false)...} }

    cmd/compile/internal/syntax.scanner?每次都會(huì)通過(guò)?cmd/compile/internal/syntax.source.nextch?函數(shù)獲取文件中最近的未被解析的字符,然后根據(jù)當(dāng)前字符的不同執(zhí)行不同的 case,如果遇到了空格和換行符這些空白字符會(huì)直接跳過(guò),如果當(dāng)前字符是 0 就會(huì)執(zhí)行?cmd/compile/internal/syntax.scanner.number?方法嘗試匹配一個(gè)數(shù)字。

    func (s *scanner) number(seenPoint bool) {kind := IntLitbase := 10 // number basedigsep := 0invalid := -1 // index of invalid digit in literal, or < 0s.kind = IntLitif !seenPoint {digsep |= s.digits(base, &invalid)}s.setLit(kind, ok) }func (s *scanner) digits(base int, invalid *int) (digsep int) {max := rune('0' + base)for isDecimal(s.ch) || s.ch == '_' {ds := 1if s.ch == '_' {ds = 2} else if s.ch >= max && *invalid < 0 {_, col := s.pos()*invalid = int(col - s.col) // record invalid rune index}digsep |= dss.nextch()}return }

    上述的?cmd/compile/internal/syntax.scanner.number?方法省略了很多的代碼,包括如何匹配浮點(diǎn)數(shù)、指數(shù)和復(fù)數(shù),我們只是簡(jiǎn)單看一下詞法分析匹配整數(shù)的邏輯:在 for 循環(huán)中不斷獲取最新的字符,將字符通過(guò)?cmd/compile/internal/syntax.source.nextch?方法追加到?cmd/compile/internal/syntax.scanner?持有的緩沖區(qū)中;

    當(dāng)前包中的詞法分析器?cmd/compile/internal/syntax.scanner?也只是為上層提供了?cmd/compile/internal/syntax.scanner.next?方法,詞法解析的過(guò)程都是惰性的,只有在上層的解析器需要時(shí)才會(huì)調(diào)用?cmd/compile/internal/syntax.scanner.next?獲取最新的 Token。

    Go 語(yǔ)言的詞法元素相對(duì)來(lái)說(shuō)還是比較簡(jiǎn)單,使用這種巨大的 switch/case 進(jìn)行詞法解析也比較方便和順手,早期的 Go 語(yǔ)言雖然使用 lex 這種工具來(lái)生成詞法解析器,但是最后還是使用 Go 來(lái)實(shí)現(xiàn)詞法分析器,用自己寫的詞法分析器來(lái)解析自己8。

    ?語(yǔ)法分析?#

    語(yǔ)法分析是根據(jù)某種特定的形式文法(Grammar)對(duì) Token 序列構(gòu)成的輸入文本進(jìn)行分析并確定其語(yǔ)法結(jié)構(gòu)的過(guò)程9。從上面的定義來(lái)看,詞法分析器輸出的結(jié)果 — Token 序列是語(yǔ)法分析器的輸入。

    語(yǔ)法分析的過(guò)程會(huì)使用自頂向下或者自底向上的方式進(jìn)行推導(dǎo),在介紹 Go 語(yǔ)言語(yǔ)法分析之前,我們會(huì)先來(lái)介紹語(yǔ)法分析中的文法和分析方法。

    文法?#

    上下文無(wú)關(guān)文法是用來(lái)形式化、精確描述某種編程語(yǔ)言的工具,我們能夠通過(guò)文法定義一種語(yǔ)言的語(yǔ)法,它主要包含一系列用于轉(zhuǎn)換字符串的生產(chǎn)規(guī)則(Production rule)10。上下文無(wú)關(guān)文法中的每一個(gè)生產(chǎn)規(guī)則都會(huì)將規(guī)則左側(cè)的非終結(jié)符轉(zhuǎn)換成右側(cè)的字符串,文法都由以下的四個(gè)部分組成:

    終結(jié)符是文法中無(wú)法再被展開的符號(hào),而非終結(jié)符與之相反,還可以通過(guò)生產(chǎn)規(guī)則進(jìn)行展開,例如 “id”、“123” 等標(biāo)識(shí)或者字面量11。

    • 𝑁N?有限個(gè)非終結(jié)符的集合;
    • ΣΣ?有限個(gè)終結(jié)符的集合;
    • 𝑃P?有限個(gè)生產(chǎn)規(guī)則12的集合;
    • 𝑆S?非終結(jié)符集合中唯一的開始符號(hào);

    文法被定義成一個(gè)四元組?(𝑁,Σ,𝑃,𝑆)(N,Σ,P,S),這個(gè)元組中的幾部分是上面提到的四個(gè)符號(hào),其中最為重要的就是生產(chǎn)規(guī)則,每個(gè)生產(chǎn)規(guī)則都會(huì)包含非終結(jié)符、終結(jié)符或者開始符號(hào),我們?cè)谶@里可以舉個(gè)簡(jiǎn)單的例子:

  • 𝑆→𝑎𝑆𝑏S→aSb
  • 𝑆→𝑎𝑏S→ab
  • 𝑆→𝜖S→?
  • 上述規(guī)則構(gòu)成的文法就能夠表示 ab、aabb 以及 aaa..bbb 等字符串,編程語(yǔ)言的文法就是由這一系列的生產(chǎn)規(guī)則表示的,在這里我們可以從?src/cmd/compile/internal/syntax/parser.go13?文件中摘抄一些 Go 語(yǔ)言文法的生產(chǎn)規(guī)則:

    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 .

    Go 語(yǔ)言更詳細(xì)的文法可以從?Language Specification14?中找到,這里不僅包含語(yǔ)言的文法,還包含詞法元素、內(nèi)置函數(shù)等信息。

    因?yàn)槊總€(gè) Go 源代碼文件最終都會(huì)被解析成一個(gè)獨(dú)立的抽象語(yǔ)法樹,所以語(yǔ)法樹最頂層的結(jié)構(gòu)或者開始符號(hào)都是 SourceFile:

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

    從 SourceFile 相關(guān)的生產(chǎn)規(guī)則我們可以看出,每一個(gè)文件都包含一個(gè)?package?的定義以及可選的?import?聲明和其他的頂層聲明(TopLevelDecl),每一個(gè) SourceFile 在編譯器中都對(duì)應(yīng)一個(gè)?cmd/compile/internal/syntax.File?結(jié)構(gòu)體,你能從它們的定義中輕松找到兩者的聯(lián)系:

    type File struct {Pragma PragmaPkgName *NameDeclList []DeclLines uintnode }

    頂層聲明有五大類型,分別是常量、類型、變量、函數(shù)和方法,你可以在文件?src/cmd/compile/internal/syntax/parser.go?中找到這五大類型的定義。

    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 ) .

    上述的文法分別定義了 Go 語(yǔ)言中常量、類型和變量三種常見的結(jié)構(gòu),從文法中可以看到語(yǔ)言中的很多關(guān)鍵字?const、type?和?var,稍微回想一下我們?nèi)粘=佑|的 Go 語(yǔ)言代碼就能驗(yàn)證這里文法的正確性。

    除了三種簡(jiǎn)單的語(yǔ)法結(jié)構(gòu)之外,函數(shù)和方法的定義就更加復(fù)雜,從下面的文法我們可以看到 Statement 總共可以轉(zhuǎn)換成 15 種不同的語(yǔ)法結(jié)構(gòu),這些語(yǔ)法結(jié)構(gòu)就包括我們經(jīng)常使用的 switch/case、if/else、for 循環(huán)以及 select 等語(yǔ)句:

    FunctionDecl = "func" FunctionName Signature [ FunctionBody ] . FunctionName = identifier . FunctionBody = Block .MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] . Receiver = Parameters .Block = "{" StatementList "}" . StatementList = { Statement ";" } .Statement =Declaration | LabeledStmt | SimpleStmt |GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |DeferStmt .SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .

    這些不同的語(yǔ)法結(jié)構(gòu)共同定義了 Go 語(yǔ)言中能夠使用的語(yǔ)法結(jié)構(gòu)和表達(dá)式,對(duì)于 Statement 展開的更多內(nèi)容這篇文章就不會(huì)詳細(xì)介紹了,感興趣的讀者可以直接查看?Go 語(yǔ)言說(shuō)明書或者直接從?src/cmd/compile/internal/syntax/parser.go?文件中找到想要的答案。

    分析方法?#

    語(yǔ)法分析的分析方法一般分為自頂向下和自底向上兩種,這兩種方式會(huì)使用不同的方式對(duì)輸入的 Token 序列進(jìn)行推導(dǎo):

    • 自頂向下分析:可以被看作找到當(dāng)前輸入流最左推導(dǎo)的過(guò)程,對(duì)于任意一個(gè)輸入流,根據(jù)當(dāng)前的輸入符號(hào),確定一個(gè)生產(chǎn)規(guī)則,使用生產(chǎn)規(guī)則右側(cè)的符號(hào)替代相應(yīng)的非終結(jié)符向下推導(dǎo)15;
    • 自底向上分析:語(yǔ)法分析器從輸入流開始,每次都嘗試重寫最右側(cè)的多個(gè)符號(hào),這其實(shí)是說(shuō)解析器會(huì)從最簡(jiǎn)單的符號(hào)進(jìn)行推導(dǎo),在解析的最后合并成開始符號(hào)16;

    如果讀者無(wú)法理解上述的定義也沒(méi)有關(guān)系,我們會(huì)在這一節(jié)的剩余部分介紹兩種不同的分析方法以及它們的具體分析過(guò)程。

    自頂向下?#

    LL 文法17是一種使用自頂向下分析方法的文法,下面給出了一個(gè)常見的 LL 文法:

  • 𝑆→𝑎𝑆1S→aS1
  • 𝑆1→𝑏𝑆1S1→bS1
  • 𝑆1→𝜖S1→?
  • 假設(shè)我們存在以上的生產(chǎn)規(guī)則和輸入流 abb,如果這里使用自頂向下的方式進(jìn)行語(yǔ)法分析,我們可以理解為每次解析器會(huì)通過(guò)新加入的字符判斷應(yīng)該使用什么方式展開當(dāng)前的輸入流:

  • 𝑆S?(開始符號(hào))
  • 𝑎𝑆1aS1(規(guī)則 1)
  • 𝑎𝑏𝑆1abS1(規(guī)則 2)
  • 𝑎𝑏𝑏𝑆1abbS1(規(guī)則 2)
  • 𝑎𝑏𝑏abb(規(guī)則 3)
  • 這種分析方法一定會(huì)從開始符號(hào)分析,通過(guò)下一個(gè)即將入棧的符號(hào)判斷應(yīng)該如何對(duì)當(dāng)前堆棧中最右側(cè)的非終結(jié)符(𝑆S?或?𝑆1S1)進(jìn)行展開,直到整個(gè)字符串中不存在任何的非終結(jié)符,整個(gè)解析過(guò)程才會(huì)結(jié)束。

    自底向上?#

    但是如果我們使用自底向上的方式對(duì)輸入流進(jìn)行分析時(shí),處理過(guò)程就會(huì)完全不同了,常見的四種文法 LR(0)、SLR、LR(1) 和 LALR(1) 使用了自底向上的處理方式18,我們可以簡(jiǎn)單寫一個(gè)與上一節(jié)中效果相同的 LR(0) 文法:

  • 𝑆→𝑆1S→S1
  • 𝑆1→𝑆1𝑏S1→S1b
  • 𝑆1→𝑎S1→a
  • 使用上述等效的文法處理同樣地輸入流 abb 會(huì)使用完全不同的過(guò)程對(duì)輸入流進(jìn)行展開:

  • 𝑎a(入棧)
  • 𝑆1S1(規(guī)則 3)
  • 𝑆1𝑏S1b(入棧)
  • 𝑆1S1(規(guī)則 2)
  • 𝑆1𝑏S1b(入棧)
  • 𝑆1S1(規(guī)則 2)
  • 𝑆S(規(guī)則 1)
  • 自底向上的分析過(guò)程會(huì)維護(hù)一個(gè)棧用于存儲(chǔ)未被歸約的符號(hào),在整個(gè)過(guò)程中會(huì)執(zhí)行兩種不同的操作,一種叫做入棧(Shift),也就是將下一個(gè)符號(hào)入棧,另一種叫做歸約(Reduce),也就是對(duì)最右側(cè)的字符串按照生產(chǎn)規(guī)則進(jìn)行合并。

    上述的分析過(guò)程和自頂向下的分析方法完全不同,這兩種不同的分析方法其實(shí)也代表了計(jì)算機(jī)科學(xué)中兩種不同的思想 — 從抽象到具體和從具體到抽象。

    Lookahead?#

    在語(yǔ)法分析中除了 LL 和 LR 這兩種不同類型的語(yǔ)法分析方法之外,還存在另一個(gè)非常重要的概念,就是向前查看(Lookahead),在不同生產(chǎn)規(guī)則發(fā)生沖突時(shí),當(dāng)前解析器需要通過(guò)預(yù)讀一些 Token 判斷當(dāng)前應(yīng)該用什么生產(chǎn)規(guī)則對(duì)輸入流進(jìn)行展開或者歸約19,例如在 LALR(1) 文法中,需要預(yù)讀一個(gè) Token 保證出現(xiàn)沖突的生產(chǎn)規(guī)則能夠被正確處理。

    Go?#

    Go 語(yǔ)言的解析器使用了 LALR(1) 的文法來(lái)解析詞法分析過(guò)程中輸出的 Token 序列20,最右推導(dǎo)加向前查看構(gòu)成了 Go 語(yǔ)言解析器的最基本原理,也是大多數(shù)編程語(yǔ)言的選擇。

    我們?cè)诟攀鲋幸呀?jīng)介紹了編譯器的主函數(shù),該函數(shù)調(diào)用的?cmd/compile/internal/gc.parseFiles?會(huì)使用多個(gè) Goroutine 來(lái)解析源文件,解析的過(guò)程會(huì)調(diào)用?cmd/compile/internal/syntax.Parse,該函數(shù)初始化了一個(gè)新的?cmd/compile/internal/syntax.parser?結(jié)構(gòu)體并通過(guò)?cmd/compile/internal/syntax.parser.fileOrNil?方法開啟對(duì)當(dāng)前文件的詞法和語(yǔ)法解析:

    func Parse(base *PosBase, src io.Reader, errh ErrorHandler, pragh PragmaHandler, mode Mode) (_ *File, first error) {var p parserp.init(base, src, errh, pragh, mode)p.next()return p.fileOrNil(), p.first }

    cmd/compile/internal/syntax.parser.fileOrNil?方法其實(shí)是對(duì)上面介紹的 Go 語(yǔ)言文法的實(shí)現(xiàn),該方法首先會(huì)解析文件開頭的?package?定義:

    // SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } . func (p *parser) fileOrNil() *File {f := new(File)f.pos = p.pos()if !p.got(_Package) {p.syntaxError("package statement must be first")return nil}f.PkgName = p.name()p.want(_Semi)

    從上面的這一段方法中我們可以看出,當(dāng)前方法會(huì)通過(guò)?cmd/compile/internal/syntax.parser.got?來(lái)判斷下一個(gè) Token 是不是?package?關(guān)鍵字,如果是?package?關(guān)鍵字,就會(huì)執(zhí)行?cmd/compile/internal/syntax.parser.name?來(lái)匹配一個(gè)包名并將結(jié)果保存到返回的文件結(jié)構(gòu)體中。

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

    確定了當(dāng)前文件的包名之后,就開始解析可選的?import?聲明,每一個(gè)?import?在解析器看來(lái)都是一個(gè)聲明語(yǔ)句,這些聲明語(yǔ)句都會(huì)被加入到文件的?DeclList?中。

    在這之后會(huì)根據(jù)編譯器獲取的關(guān)鍵字進(jìn)入 switch 的不同分支,這些分支調(diào)用?cmd/compile/internal/syntax.parser.appendGroup?方法并在方法中傳入用于處理對(duì)應(yīng)類型語(yǔ)句的?cmd/compile/internal/syntax.parser.constDecl、cmd/compile/internal/syntax.parser.typeDecl?函數(shù)。

    for p.tok != _EOF {switch p.tok {case _Const:p.next()f.DeclList = p.appendGroup(f.DeclList, p.constDecl)case _Type:p.next()f.DeclList = p.appendGroup(f.DeclList, p.typeDecl)case _Var:p.next()f.DeclList = p.appendGroup(f.DeclList, p.varDecl)case _Func:p.next()if d := p.funcDeclOrNil(); d != nil {f.DeclList = append(f.DeclList, d)}default:...}}f.Lines = p.source.linereturn f }

    cmd/compile/internal/syntax.parser.fileOrNil?使用了非常多的子方法對(duì)輸入的文件進(jìn)行語(yǔ)法分析,并在最后會(huì)返回文件開始創(chuàng)建的?cmd/compile/internal/syntax.File?結(jié)構(gòu)體。

    讀到這里的人可能會(huì)有一些疑惑,為什么沒(méi)有看到詞法分析的代碼,這是因?yàn)樵~法分析器?cmd/compile/internal/syntax.scanner?作為結(jié)構(gòu)體被嵌入到了?cmd/compile/internal/syntax.parser?中,所以這個(gè)方法中的?p.next()?實(shí)際上調(diào)用的是?cmd/compile/internal/syntax.scanner.next?方法,它會(huì)直接獲取文件中的下一個(gè) Token,所以詞法和語(yǔ)法分析一起進(jìn)行的。

    cmd/compile/internal/syntax.parser.fileOrNil?與在這個(gè)方法中執(zhí)行的其他子方法共同構(gòu)成了一棵樹,這棵樹根節(jié)點(diǎn)是?cmd/compile/internal/syntax.parser.fileOrNil,子節(jié)點(diǎn)是?cmd/compile/internal/syntax.parser.importDecl、cmd/compile/internal/syntax.parser.constDecl?等方法,它們與 Go 語(yǔ)言文法中的生產(chǎn)規(guī)則一一對(duì)應(yīng)。

    圖 2-8 Go 語(yǔ)言解析器的方法

    cmd/compile/internal/syntax.parser.fileOrNil、cmd/compile/internal/syntax.parser.constDecl?等方法對(duì)應(yīng)了 Go 語(yǔ)言中的生產(chǎn)規(guī)則,例如?cmd/compile/internal/syntax.parser.fileOrNil?實(shí)現(xiàn)的是:

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

    我們根據(jù)這個(gè)規(guī)則能很好地理解語(yǔ)法分析器的實(shí)現(xiàn)原理 - 將編程語(yǔ)言的所有生產(chǎn)規(guī)則映射到對(duì)應(yīng)的方法上,這些方法構(gòu)成的樹形結(jié)構(gòu)最終會(huì)返回一個(gè)抽象語(yǔ)法樹。

    因?yàn)榇蠖鄶?shù)方法的實(shí)現(xiàn)都非常相似,所以這里就僅介紹?cmd/compile/internal/syntax.parser.fileOrNil?方法的實(shí)現(xiàn)了,想要了解其他方法的實(shí)現(xiàn)原理,讀者可以自行查看?src/cmd/compile/internal/syntax/parser.go?文件,該文件包含了語(yǔ)法分析階段的全部方法。

    輔助方法?#

    雖然這里不會(huì)展開介紹其他類似方法的實(shí)現(xiàn),但是解析器運(yùn)行過(guò)程中有幾個(gè)輔助方法我們還是要簡(jiǎn)單說(shuō)明一下,首先就是?cmd/compile/internal/syntax.parser.got?和?cmd/compile/internal/syntax.parser.want?這兩個(gè)常見的方法:

    func (p *parser) got(tok token) bool {if p.tok == tok {p.next()return true}return false }func (p *parser) want(tok token) {if !p.got(tok) {p.syntaxError("expecting " + tokstring(tok))p.advance()} }

    cmd/compile/internal/syntax.parser.got?只是用于快速判斷一些語(yǔ)句中的關(guān)鍵字,如果當(dāng)前解析器中的 Token 是傳入的 Token 就會(huì)直接跳過(guò)該 Token 并返回?true;而?cmd/compile/internal/syntax.parser.want?就是對(duì)?cmd/compile/internal/syntax.parser.got?的簡(jiǎn)單封裝了,如果當(dāng)前 Token 不是我們期望的,就會(huì)立刻返回語(yǔ)法錯(cuò)誤并結(jié)束這次編譯。

    這兩個(gè)方法的引入能夠幫助工程師在上層減少判斷關(guān)鍵字的大量重復(fù)邏輯,讓上層語(yǔ)法分析過(guò)程的實(shí)現(xiàn)更加清晰。

    另一個(gè)方法?cmd/compile/internal/synctax.parser.appendGroup?的實(shí)現(xiàn)就稍微復(fù)雜了一點(diǎn),它的主要作用就是找出批量的定義,我們可以簡(jiǎn)單舉一個(gè)例子:

    var (a intb int )

    這兩個(gè)變量其實(shí)屬于同一個(gè)組(Group),各種頂層定義的結(jié)構(gòu)體?cmd/compile/internal/syntax.parser.constDecl、cmd/compile/internal/syntax.parser.varDecl?在進(jìn)行語(yǔ)法分析時(shí)有一個(gè)額外的參數(shù)?cmd/compile/internal/syntax.Group,這個(gè)參數(shù)是通過(guò)?cmd/compile/internal/syntax.parser.appendGroup?方法傳遞進(jìn)去的:

    func (p *parser) appendGroup(list []Decl, f func(*Group) Decl) []Decl {if p.tok == _Lparen {g := new(Group)p.list(_Lparen, _Semi, _Rparen, func() bool {list = append(list, f(g))return false})} else {list = append(list, f(nil))}return list }

    cmd/compile/internal/syntax.parser.appendGroup?方法會(huì)調(diào)用傳入的?f?方法對(duì)輸入流進(jìn)行匹配并將匹配的結(jié)果追加到另一個(gè)參數(shù)?cmd/compile/internal/syntax.File?結(jié)構(gòu)體中的?DeclList?數(shù)組中,import、const、var、type?和?func?聲明語(yǔ)句都是調(diào)用?cmd/compile/internal/syntax.parser.appendGroup?方法解析的。

    節(jié)點(diǎn)?#

    語(yǔ)法分析器最終會(huì)使用不同的結(jié)構(gòu)體來(lái)構(gòu)建抽象語(yǔ)法樹中的節(jié)點(diǎn),其中根節(jié)點(diǎn)?cmd/compile/internal/syntax.File?我們已經(jīng)在上面介紹過(guò)了,其中包含了當(dāng)前文件的包名、所有聲明結(jié)構(gòu)的列表和文件的行數(shù):

    type File struct {Pragma PragmaPkgName *NameDeclList []DeclLines uintnode }

    src/cmd/compile/internal/syntax/nodes.go?文件中也定義了其他節(jié)點(diǎn)的結(jié)構(gòu)體,其中包含全部聲明類型的,這里簡(jiǎn)單看一下函數(shù)聲明的結(jié)構(gòu):

    type (Decl interface {NodeaDecl()}FuncDecl struct {Attr map[string]boolRecv *FieldName *NameType *FuncTypeBody *BlockStmtPragma Pragmadecl} }

    從函數(shù)定義中我們可以看出,函數(shù)在語(yǔ)法結(jié)構(gòu)上主要由接受者、函數(shù)名、函數(shù)類型和函數(shù)體幾個(gè)部分組成,函數(shù)體?cmd/compile/internal/syntax.BlockStmt?是由一系列的表達(dá)式組成的,這些表達(dá)式共同組成了函數(shù)的主體:

    圖 2-9 Go 語(yǔ)言函數(shù)定義的結(jié)構(gòu)體

    函數(shù)的主體其實(shí)是一個(gè)?cmd/compile/internal/syntax.Stmt?數(shù)組,cmd/compile/internal/syntax.Stmt?是一個(gè)接口,實(shí)現(xiàn)該接口的類型其實(shí)也非常多,總共有 14 種不同類型的?cmd/compile/internal/syntax.Stmt?實(shí)現(xiàn):

    圖 2-9 Go 語(yǔ)言的 14 種聲明

    這些不同類型的?cmd/compile/internal/syntax.Stmt?構(gòu)成了全部命令式的 Go 語(yǔ)言代碼,從中我們可以看到很多熟悉的控制結(jié)構(gòu),例如 if、for、switch 和 select,這些命令式的結(jié)構(gòu)在其他的編程語(yǔ)言中也非常常見。

    2.2.3 小結(jié)?#

    這一節(jié)介紹了 Go 語(yǔ)言的詞法分析和語(yǔ)法分析過(guò)程,我們不僅從理論的層面介紹了詞法和語(yǔ)法分析的原理,還從源代碼出發(fā)詳細(xì)分析 Go 語(yǔ)言的編譯器是如何在底層實(shí)現(xiàn)詞法和語(yǔ)法解析功能的。

    了解 Go 語(yǔ)言的詞法分析器?cmd/compile/internal/syntax.scanner?和語(yǔ)法分析器?cmd/compile/internal/syntax.parser?讓我們對(duì)解析器處理源代碼的過(guò)程有著比較清楚的認(rèn)識(shí),同時(shí)我們也在 Go 語(yǔ)言的文法和語(yǔ)法分析器中找到了熟悉的關(guān)鍵字和語(yǔ)法結(jié)構(gòu),加深了對(duì) Go 語(yǔ)言的理解。

    2.2.4 延伸閱讀?#

    • Lexical Scanning in Go - Rob Pike


  • 通用編程語(yǔ)言 General-purpose programming language?https://en.wikipedia.org/wiki/General-purpose_programming_language???

  • 詞法分析 Lexical analysis?https://en.wikipedia.org/wiki/Lexical_analysis???

  • 詞法分析生成器?Lex - A Lexical Analyzer Generator???

  • 生成的 simplego.lex.c 文件?https://gist.github.com/draveness/85db6ec4a4088b63ccccf7f09424f474???

  • 有限自動(dòng)機(jī) DFA?https://en.wikipedia.org/wiki/Deterministic_finite_automaton???

  • go/scanner.go at master · golang/go · GitHub???

  • go/tokens.go at master · golang/go · GitHub???

  • Go 1.5 Bootstrap Plan?https://docs.google.com/document/d/1OaatvGhEAq7VseQ9kkavxKNAfepWy2yhPUBs96FGV28/edit???

  • 語(yǔ)法分析 Syntactic analysis?https://en.wikipedia.org/wiki/Parsing???

  • https://en.wikipedia.org/wiki/Context-free_grammar???

  • 終結(jié)符和非終結(jié)符?https://en.wikipedia.org/wiki/Terminal_and_nonterminal_symbols???

  • 生產(chǎn)規(guī)則在計(jì)算機(jī)科學(xué)領(lǐng)域是符號(hào)替換的重寫規(guī)則,S -> aSb 就是可以用右側(cè)的 aSb 將左側(cè)的符號(hào)進(jìn)行展開?https://en.wikipedia.org/wiki/Production_(computer_science)???

  • go/parser.go at master · golang/go · GitHub???

  • The Go Programming Language Specification?https://golang.org/ref/spec???

  • 自頂向下解析?https://en.wikipedia.org/wiki/Top-down_parsing???

  • 自底向上解析?https://en.wikipedia.org/wiki/Bottom-up_parsing???

  • LL 文法是一種上下文無(wú)關(guān)文法,可以使用 LL 解析器解析?https://en.wikipedia.org/wiki/LL_grammar???

  • LR 解析器是一種自底向上的解析器,它有很多 SLR、LALR、等變種?https://en.wikipedia.org/wiki/LR_parser???

  • https://en.wikipedia.org/wiki/Lookahead???

  • 關(guān)于 Go 語(yǔ)言文法的討論?https://groups.google.com/forum/#!msg/golang-nuts/jVjbH2-emMQ/UdZlSNhd3DwJ???

  • 關(guān)于 GO 語(yǔ)言的類型檢查可參見:Go 語(yǔ)言如何進(jìn)行類型檢查 | Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn)?

    關(guān)于 GO 語(yǔ)言的中間代碼生成可參見:詳解 Go 語(yǔ)言中間代碼生成 | Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn)

    ?關(guān)于 GO 語(yǔ)言的機(jī)器碼生成可參見:指令集架構(gòu)、機(jī)器碼與 Go 語(yǔ)言 | Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn)

    轉(zhuǎn)載自:?Go 語(yǔ)言編譯過(guò)程概述 | Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn)?

    總結(jié)

    以上是生活随笔為你收集整理的(四)Go 语言编译流程简述的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。