跳轉到

語意分析與符號表:型態檢查與作用域#

語法解析完成,編譯器手上有一棵 AST。但這棵樹只知道「形狀」,不知道「意義」:x + 1 中的 x 是整數還是矩陣?f(3) 中的 f 有沒有宣告過?在 AST 能被翻譯成中間碼之前,編譯器必須先通過語意分析——遍歷樹,管理名稱,推導型態,確認每一個符號都有合法的所指。這一階段的工具簡單(一張符號表),背後的思想史卻異常深遠。

符號表與區塊結構#

最早的符號表只是陣列加線性搜尋。讓它複雜化的是 ALGOL 60 的區塊結構(block structure):程式裡可以嵌套宣告 begin/end 區塊,內層可以遮蔽(shadow)外層的同名變數,離開區塊後宣告即失效。這是程式語言史上第一次把「作用域的生命週期」嚴格嵌套化,Knuth 在 1967 年的著名長文〈The Remaining Troublespots in ALGOL 60〉中仔細剖析過它的語意陷阱。

工程上的回答優雅至極:符號表做成雜湊表的堆疊。進入區塊就壓入一層,離開就彈出;查名稱從頂層往下找,第一個命中即生效。作用域規則由此從一段冗長的文字定義,變成一個人人可以驗證的資料結構。雜湊技術本身也在這個時代快速成熟——Knuth 第三卷《Sorting and Searching》(1973) 對雜湊的系統化論述,正是反饋自編譯器與組譯器的實務需求。

型態系統的譜系#

型態檢查的歷史是一連串語言設計者的抉擇。

ALGOL 60 首次引入型態宣告:integer、real、boolean。型態的動機最初很樸素——告訴編譯器要分配幾個字、生成哪種機器指令。Simula 67(Ole-Johan Dahl 與 Kristen Nygaard)為了模擬世界,引入 class 與物件,型態開始承載「資料的行為分類」這層新意義。Pascal(Niklaus Wirth, 1970)把強型態推到極致。Wirth 的哲學是「型別即文檔」:每一個自訂型別都該有一個名字,說明它代表什麼;能讓編譯器抓住的錯誤,就不要等到執行期。他寧可程式設計師多寫幾行宣告,換取編譯器成為第一道品質關卡。這種「透過限制獲得自由」的設計觀,直接塑造了 Ada、Modula,也成為靜態型態陣營的思想綱領。

ML 則是另一場革命的起點。1973 年起,Robin Milner 在史丹佛與愛丁堡為 LCF 定理證明器開發 Meta Language。為了保證定理證明器中「策略程式碼」不會污染「證明邏輯」,他設計了型態系統;更驚人的是,1978 年他與 Luis Damas 發表的 Hindley–Milner 型態推論演算法 W:程式設計師可以完全不寫任何型態宣告,編譯器靠單一化(unification)——一種解方程式的技術——自動推導出所有表達式的型態,並在編譯期證明不會有型態錯誤。這打破了「靜態型態等於繁瑣」的刻板印象,讓靜態型態與簡潔可以兼得。Haskell、OCaml、F#、Rust 的型態系統,全是這棵樹的枝幹。

靜態 vs 動態:一場哲學戰爭#

「錯誤應該在編譯期抓還是執行期抓」——這個技術問題在 1960 年代以後演變成一場橫跨五十年的陣營戰爭。靜態陣營(Pascal、Ada、ML、Java)相信型別是契約,愈早驗證愈好;動態陣營(Lisp、Smalltalk、Python、JavaScript)相信型別標註是儀式負擔,靈活性與快速迭代更重要。這場戰爭與其說是技術辯論,不如說是兩種工程文化的對撞:把軟體視為需要形式保證的結構工程,還是視為需要快速演化的生物。歷史的最終走向是融合——漸進型態(TypeScript、mypy、Hack)讓開發者自選強度,而 JVM 與 CPython 內部都偷偷用型態資訊做 JIT 優化:型態不只是安全,也是速度。

語意分析的實務#

回到編譯器內部,語意分析的日常工作包括:名稱解析(把每個識別字連到它的宣告,處理遮蔽與 import)、型態檢查與推導(包括隱式轉換規則,如 C 的整數提升)、函式過載決議(同名函式依引數型態擇一,C++ 的重載決議規則複雜到出了專書)、以及可存取性檢查(public/private、模組可見性)。每一步都在符號表上進行,每一步的錯誤訊息品質——「第 42 行:x 未定義;你是否想用第 38 行的 x?」——直接決定一門語言的使用者體驗。Rust 編譯器以錯誤訊息聞名,投資的正是這一環。

型態即命題#

最深的傳承來自邏輯學。1958 年前後,邏輯學家 Haskell Curry 與 William Howard 分別注意到一個奇妙的對應:直覺主義邏輯中的命題證明,與帶型態的 lambda 演算中的程式,結構完全同構。一個型態就是一個命題,一個型態正確的程式就是一個證明——所謂 Curry–Howard 對應。這個 1970 年代由 Howard 的論文發揚的洞見,起初只是邏輯學圈內的優雅觀察,如今已是型態理論的哲學根基:ML 的 let 多型對應邏輯的全稱量化、依值型態(Coq、Agda)對應更強的邏輯系統、而 Rust 的生命週期檢查在某種意義上是區域邏輯。從希爾伯特學派的形式主義,到 Martin-Löf 的建構類型論,再到今日的程式語言理論學系,一條跨越百年的思想鏈條,最終落進了每一個編譯器的型態檢查器裡。

語意分析結束後,AST 已經「有了意義」。但機器不認識樹。編譯器必須把這棵樹翻譯成某種更接近機器的形式——這個翻譯的落點如何選擇,正是下一節的主題:中間表示。