Rust 的錯(cuò)誤信息輸出原理
原文地址:https://mp.weixin.qq.com/s/eGS95WGU814RdD6Vs3J6yQ

1. 背景
最近在參與 KusionStack 內(nèi)置的領(lǐng)域語(yǔ)言 —— KCL 配置語(yǔ)言編譯器 的開發(fā),需要開發(fā)編譯器的錯(cuò)誤處理模塊,由于 KCL 使用 Rust 開發(fā)的,因此打算來學(xué)學(xué) Rust 語(yǔ)言的錯(cuò)誤處理模塊是怎么做的。
2. 介紹
單純從 Rustc 源碼的目錄結(jié)構(gòu)中看,Rustc 中關(guān)于錯(cuò)誤處理的部分主要集中在 rustc_errors 、rustc_error_codes 和 rustc_error_message 三個(gè)目錄下,但是在看源碼的過程中我發(fā)現(xiàn)由于 Rustc 代碼量大,并且錯(cuò)誤處理模塊涉及到很多其他的模塊,單純的看這三個(gè)目錄下的代碼很容易看暈,剖析起來也比較困難。因此,我打算將這部分的的源碼剖析拆分成幾個(gè)部分,這篇文章主要結(jié)合 Rustc 的官方文檔和 Rustc 源碼進(jìn)行結(jié)構(gòu)的梳理。
因此本文的核心思路只是對(duì)錯(cuò)誤處理部分的結(jié)構(gòu)進(jìn)行梳理,目標(biāo)就是梳理一下在 Rustc 對(duì) Rust 程序進(jìn)行解析的過程中,錯(cuò)誤是如何從分析過程一步一步傳遞到終端輸出成診斷信息的。對(duì)于一些復(fù)雜且與錯(cuò)誤診斷信息輸出無關(guān)的內(nèi)容,我們先暫且略過不做深究。留個(gè)坑后面再填,先把結(jié)構(gòu)梳理清楚,也有助于我們后續(xù)一步一步的對(duì)源碼進(jìn)行更加深入清晰的剖析,防止我們?cè)?Rustc 大量的源碼中迷路。并且為了能更加清晰的看代碼的結(jié)構(gòu),本文對(duì)使用的代碼片段做了處理,去掉了生命周期等與代碼執(zhí)行邏輯無關(guān)的部分。
3. 診斷信息長(zhǎng)啥樣?
首先,看源碼之前,先看看 Rust 的診斷信息的格式。如下圖所示:

根據(jù) Rustc 文檔中的描述,上述信息可以分為下面5個(gè)部分,
Level 等級(jí) (錯(cuò)誤,警告等等),這部分主要用來說明當(dāng)前消息的嚴(yán)重程度。
Code 代碼或者翻譯成編號(hào)更好一些 (例如:對(duì)于“錯(cuò)誤的類型”這種診斷信息,它對(duì)應(yīng)的編號(hào)是E0308),這個(gè)編號(hào)是一個(gè)索引,用戶可以通過這個(gè)索引找到當(dāng)前錯(cuò)誤更加完整的描述信息。通過 lint 創(chuàng)建的診斷信息沒有這個(gè)編號(hào)。?
注:我后面又查了一下,rustc 官方把 Code 稱作 Rust Compiler Error Index。Message 消息,描述當(dāng)前發(fā)生的問題的主要內(nèi)容,這個(gè)消息的內(nèi)容應(yīng)該是通用的獨(dú)立的,即使沒有其他內(nèi)容只看這一條信息的話,也能有所幫助。
Diagnostic Window 診斷窗口,主要負(fù)責(zé)展示出現(xiàn)問題的代碼上下文相關(guān)的信息。
Sub-diagnostic 子診斷信息,任何錯(cuò)誤都有很多的子診斷信息并且他們看起來都和診斷信息的主部分相似。
4. 診斷信息從哪來?
在了解了 Rustc 診斷信息之后,我們看下 Rustc 是如何構(gòu)造這樣的診斷信息的。在這部分 Rustc 官方提供了兩種方式,
實(shí)現(xiàn) rustc_sessions 提供的 trait。
用 rustc_macros 中為輸出診斷信息準(zhǔn)備的屬性宏,自動(dòng)實(shí)現(xiàn) rustc_sessions 提供的 trait。
直接看上面這兩點(diǎn)不太好理解,主要的流程可以參考下面這張圖,

其中,黃色部分表示在 Rustc 的不同模塊中,定義各自的錯(cuò)誤/警告等異常類型的結(jié)構(gòu)體 Struct ?(注:枚舉也可以,本文是一個(gè)概述,為了方便描述所以下面就只列Struct了)。綠色部分表示在Rustc的錯(cuò)誤處理模塊提供了一個(gè) trait SessionDiagnostic。不同模塊內(nèi)部定義的 Struct 實(shí)現(xiàn)這個(gè) trait SessionDiagnostic。trait SessionDiagnostic 的具體實(shí)現(xiàn)就是將 Struct 中輸出診斷信息需要的內(nèi)容抽取出來封裝好,返回給 Rustc 的錯(cuò)誤處理模塊用來輸出。
這就是上面提到的實(shí)現(xiàn)錯(cuò)誤模塊提供的 trait。這個(gè) trait SessionDiagnostic 的源碼如下:
以 Rustc 文檔中給出的錯(cuò)誤結(jié)構(gòu)為例:
按照 Rustc 的官方描述,要想輸出 struct FieldAlreadyDeclared 對(duì)應(yīng)的錯(cuò)誤信息,就要實(shí)現(xiàn) trait SessionDiagnostic。Rustc 的源碼內(nèi)部定義的錯(cuò)誤結(jié)構(gòu)目前完全采用第二種方式。
在 Rustc 提供的官方文檔上,提供了 trait SessionDiagnostic 的具體實(shí)現(xiàn)。
上面代碼展示了如何為 Struct FieldAlreadyDeclared 實(shí)現(xiàn) trait SessionDiagnostic,具體的代碼細(xì)節(jié)看不懂也不用急,這里只做一個(gè)展示,代碼的細(xì)節(jié)不是我們本文的主題,過早的深入代碼細(xì)節(jié)容易迷路,只要知道這部分代碼從 Struct FieldAlreadyDeclared 抽取出了輸出診斷信息需要的內(nèi)容,并封裝到了 DiagnosticBuilder 中返回。
那么怎么理解第二種方式呢?以上面的代碼為例,實(shí)現(xiàn) trait SessionDiagnostic 主要是將 Struct FieldAlreadyDeclared 中需要輸出到診斷信息中的內(nèi)容,抽取出來,填充到 DiagnosticBuilder 中,這個(gè)過程其實(shí)就是在搬磚,將組成診斷信息的磚塊從 Struct FieldAlreadyDeclared 搬運(yùn)到 DiagnosticBuilder 中,因此,這個(gè)過程可以自動(dòng)化,當(dāng)我們定義一個(gè)新的錯(cuò)誤 Struct 的時(shí)候,磚塊不需要我們自己搬,我們可以寫一個(gè)程序幫我們搬,我們只需要在定義 Struct 的時(shí)候標(biāo)注出來哪些磚需要搬就可以了。
所以,Rustc 內(nèi)部通過屬性宏的方式寫好了搬磚的程序,這個(gè)搬磚程序?yàn)槲覀兲峁┝艘恍┳⒔猓诙x新的錯(cuò)誤 Struct 時(shí),只需要通過注解標(biāo)注出哪些磚要搬,Rustc 內(nèi)部的屬性宏就會(huì)為我們自動(dòng)實(shí)現(xiàn) trait SessionDiagnostic。同樣是 Struct FieldAlreadyDeclared,使用第二種方式的代碼如下:
其中,通過注解 #[derive(SessionDiagnostic)] 使用 rustc_sessions 內(nèi)部實(shí)現(xiàn)的屬性宏,通過注解[diag(typeck::field_already_declared, code = "E0124")] 說明當(dāng)前診斷信息輸出的文本信息與前面提到的當(dāng)前診斷信息的編號(hào),最后通過注解 #[primary_span], #[label] 和 #[label(typeck::previous_decl_label)] 注解標(biāo)注了出現(xiàn)問題的代碼上下文相關(guān)的信息。
定義了帶有注解的 Struct 或者為 Struct 實(shí)現(xiàn)了 trait SessionDiagnostic 后,接下來要做什么?Rustc 文檔是這么說的。
Now that we've defined our diagnostic, how do we use it? It's quite straightforward, just create an instance of the struct and pass it to emit_err (or emit_warning).
現(xiàn)在,我們已經(jīng)定義了我們的診斷信息,那我們?nèi)绾问褂盟??這非常簡(jiǎn)單,我們只需要?jiǎng)?chuàng)建一個(gè)結(jié)構(gòu)體的實(shí)例,并將它傳遞給 emit_err() 或者 emit_warning() 方法就可以了。
不太明白,但是得到了一個(gè)關(guān)鍵方法 emit_err() ,通過這個(gè)方法將錯(cuò)誤的診斷信息輸出到終端,那就在源碼里全局搜索一下這個(gè)方法:

找到了這個(gè)方法的定義如下:
我們順著方法的調(diào)用鏈路連續(xù)點(diǎn)進(jìn)去看看,
看代碼好像明白了,把上面錯(cuò)誤處理過程的圖細(xì)化一下:

如圖所示,我在圖的右面增加了一些東西,黃色的部分沒有太大的變化,Rustc 其他的模塊定義錯(cuò)誤的 Struct,綠色的部分增加了一些內(nèi)容,細(xì)化了 trait SessionDiagnostic 的主要實(shí)現(xiàn),根據(jù)黃色的 Struct 提供的內(nèi)容生成藍(lán)色的 DiagnosticBuilder。生成的 DiagnosticBuilder 中,內(nèi)置 emit() 方法用來將診斷信息輸出到終端,這個(gè) emit() 方法最后會(huì)在 Session 中被調(diào)用。
在 rustc 中通過 Struct Session 調(diào)用生成的 DiagnosticBuilder 來輸出診斷信息,具體的調(diào)用過程如上圖右側(cè)所示,Struct Session 內(nèi)置了 Struct ParseSess ,這里包了兩層 emit_err() 方法,并且在方法 ParseSess.emit_err() 中,調(diào)用了 ParseSess.create_err() 方法,這個(gè)方法接受 trait SessionDiagnostic 的實(shí)現(xiàn),并調(diào)用 trait SessionDiagnostic 提供的 into_diagnostic() 方法獲取 DiagnosticBuilder 實(shí)例,隨后調(diào)用 DiagnosticBuilder 內(nèi)置的 emit() 方法將診斷信息輸出到終端。
看到這里,問題又來了,Rustc 通過 Session 接收 DiagnosticBuilder 輸出診斷信息,這個(gè) Session 是什么?這個(gè) Session 是如何與 Rustc 其他模塊聯(lián)動(dòng)的呢?或者說這個(gè) Session 是怎么被調(diào)用的呢?
關(guān)于 Session 是什么,這不是本文的重點(diǎn),為了防止迷路,這里先刨個(gè)坑,后續(xù)的文章中看看 Session 是什么,接下來,我們先來看看 Session 是怎么被調(diào)用來處理錯(cuò)誤的。我們?cè)谌炙阉饕幌?sess.emit_err() 這個(gè)關(guān)鍵字,看看 rustc 是如何通過 Session 輸出診斷信息的。
可以看到,在Rustc中很多地方都通過 Session 輸出錯(cuò)誤信息。

我看了一下,挑了幾個(gè)其中比較典型,見名知意的地方。首先是在 Ructc 的語(yǔ)法解析器 rustc_parse 中,在進(jìn)行語(yǔ)法分析的過程中遇到錯(cuò)誤,會(huì)通過 sess.emit_err() 方法輸出錯(cuò)誤的診斷信息。

然后,在 rustc 的類型檢查器 TypeChecker 中,所有權(quán)借用檢查 rustc_borrowck 部分和類型檢查部分 rustc_typeck 在檢查到錯(cuò)誤時(shí)會(huì)通過 sess.emit_err() 方法輸出錯(cuò)誤的診斷信息。與 rustc_parse 不同的是 TypeChecker 并不直接將 Session 實(shí)例作為結(jié)構(gòu)體成員而是通過一個(gè)獲取上下文的方法 tcx() 獲取 Session 實(shí)例。


這個(gè)上下文方法 tcx() 的細(xì)節(jié)以及上下文的結(jié)構(gòu)也是暫不深究,目前我們只需要知道 TypeChecker 也是通過 Session 輸出診斷信息的就夠了。然后,我們來淺看一下他們是如何借助 Session 輸出錯(cuò)誤的信息的。
首先,看看 rustc_parse 中關(guān)于 Session 的部分:
見名知意給我?guī)砹艘稽c(diǎn)誤判, Parser 內(nèi)置的是 ParseSess 而不是 Session。所以,可以借助上面那個(gè)圖的結(jié)構(gòu),給 Parser 錯(cuò)誤處理的局部也單獨(dú)畫一張圖。

之前的圖中已經(jīng)展示了內(nèi)部的細(xì)節(jié),這里就不展示了,這里只展示 trait SessionDiagnostic 和 Parser 之間的關(guān)系,(注:上圖中的 Parse() 方法是我起的名字,指的是 Rustc中 對(duì) Rust 程序語(yǔ)法分析的過程,在 Rustc 源程序中這個(gè)方法并不一定存在,具體用的是什么方法不是本文的重點(diǎn),但是只要是編譯器就一定有 parse 過程,在不同的編譯器中 parse 過程的名字可能不同。)
如圖所示,在對(duì) Rust 程序進(jìn)行語(yǔ)法分析的過程中,如果出現(xiàn)錯(cuò)誤,就實(shí)例化一個(gè)實(shí)現(xiàn)了 trait SessionDiagnostic 的錯(cuò)誤 Struct 結(jié)構(gòu),并把它拋給 Parser 內(nèi)置的 ParseSess 中的 emit_err() 方法將診斷信息輸出。
然后,再看看 rustc_borrowck 和 rustc_typeck,從調(diào)用方式來看,他們不是直接內(nèi)置 Session 的,他們應(yīng)該是內(nèi)置了一個(gè)上下文相關(guān)的結(jié)構(gòu),然后那個(gè)上下文相關(guān)的結(jié)構(gòu)中包含 Session 。
點(diǎn)進(jìn) self 看一下,可以看到這是一個(gè)類型檢查器 TypeChecker ,找到上下文結(jié)構(gòu)并點(diǎn)進(jìn)去深度優(yōu)先的搜索 Session 或者 ParseSess 結(jié)構(gòu),為了防止大家看的時(shí)候迷路,搜索過程就不寫了,這里直接展示搜索結(jié)果。
藏的夠深的,不過好在我們還是把它挖了出來,目前聚焦于錯(cuò)誤處理,所以暫時(shí)不用關(guān)心這些上下文結(jié)構(gòu) (XXXCtxt) 都是什么意思。

如上圖所示,與 Parser 的部分同理,ty_check() 是我自己寫的方法,代指 TypeChecker 對(duì) Rust 程序進(jìn)行類型檢查的過程,目前聚焦于錯(cuò)誤處理,所以 InferCtxt,TyCtxt 和 GlobalCtxt 等上下文結(jié)構(gòu)我就縮寫為 XXXCtx 了,可以看到,這個(gè)過程和 Parser 錯(cuò)誤處理的過程是一樣的,在類型檢查的過程中出現(xiàn)錯(cuò)誤,就實(shí)例化一個(gè)實(shí)現(xiàn)了 trait SessionDiagnostic 的結(jié)構(gòu),并把它拋給 TypeChecker 內(nèi)置的各種上下文中內(nèi)置的 Session 中的 emit_err() 方法將診斷信息輸出。
看到這里,壓力來到了 Session 和 ParseSess 這邊,既然大家都把錯(cuò)誤拋給他,那就來看看它里面干了啥。
看不太明白,再把之前的代碼拿來看看
展開上述第21行的代碼,看到這是一個(gè) trait 的抽象接口:
為了防止迷路,先不深究 EmissionGuarantee 是做什么的,只關(guān)注他提供的輸出診斷信息到終端的功能就好了。
然后,我們?cè)谌炙阉?EmissionGuarantee,找一個(gè) EmissionGuarantee 的實(shí)現(xiàn),看看他是如何輸出信息的。
看到上面的代碼,我覺得壓力來到了 DiagnosticBuilder 這邊,來都來了,得看看。
可以看到,最后是通過 DiagnosticBuilderState 中的 Handler 輸出的診斷信息。
到 Handler 這里,看看注釋,我覺得可以了,我們知道了所有錯(cuò)誤的診斷信息,最后都通過 Handler 輸出到終端,到這里,可以再把上面的圖細(xì)化一下:

如圖所示,我們?cè)趫D中將 DiagnosticBuilder 內(nèi)部的一點(diǎn)點(diǎn)細(xì)節(jié)畫進(jìn)去了,先不考慮 EmissionGuarantee。DiagnosticBuilder 中包含輸出診斷信息的 Handler 和保存診斷信息內(nèi)容的 Diagnostic ,在 Session 和 ParseSess 中,會(huì)先調(diào)用 SessionDiagnostic 的 into_diagnostic() 方法,獲得 DiagnosticBuilder,然后調(diào)用 DiagnoaticBuilder 的 emit() 方法輸出診斷信息,在 emit() 方法中,會(huì)調(diào)用 DiagnoaticBuilder 內(nèi)置的 Handler 并將 DiagnoaticBuilder 中的 ?Diagnostic 輸出到終端。
總結(jié)
在本文中我們只涉獵了 Rustc 中錯(cuò)誤處理模塊很小的一部分,通過這一部分的淺看,我們大概的了解了一下 Rustc 中錯(cuò)誤從出現(xiàn)到變成診斷信息輸出到終端的整個(gè)流程。最后以上文中提到的 rustc_parser 和 rustc_type_checker 為例,一張圖收尾。

Rustc 錯(cuò)誤處理模塊的三部分:
編譯器的各個(gè)部分自定義錯(cuò)誤的結(jié)構(gòu),保存錯(cuò)誤信息。
SessionDiagnostic 負(fù)責(zé)將各部分自定義錯(cuò)誤的結(jié)構(gòu)轉(zhuǎn)換為 DiagnosticBuilder。
Session/ParseSess 負(fù)責(zé)調(diào)用 SessionDiagnostic 提供的接口獲得 DiagnosticBuilder ,并調(diào)用 DiagnosticBuilder 內(nèi)置的方法輸出診斷信息。
如果還是有一點(diǎn)繞暈了,在上面這個(gè)圖上再加一筆,通過紅色的尖頭我們可以看到 Rust 中的一個(gè)異常包含的信息的從發(fā)生錯(cuò)誤的地方到開發(fā)者面前的主要流向:

從上圖右面的部分可以看到,錯(cuò)誤信息并不是直接從 DiagnosticBuilder 中發(fā)送到開發(fā)者面前的,而是先從 Session 兜了個(gè)圈子,那為什么要這么做呢?這里先刨個(gè)坑,后續(xù)我們將進(jìn)一步深入到 Rustc 的源碼當(dāng)中去,詳細(xì)剖析解讀一下各部分的源碼結(jié)構(gòu)并且理解一下 Rustc 的開發(fā)者增加各個(gè)部分的動(dòng)機(jī)。
本期挖坑
Session 和 ParseSess 到底是什么 ?
為什么搜索 emit_err() 沒有涉及到詞法分析 Lexer 和代碼生成 CodeGen 的部分,這兩個(gè)部分的錯(cuò)誤是怎么處理的 ?
EmissionGuarantee 這個(gè)結(jié)構(gòu)在錯(cuò)誤處理的過程中是做什么的 ?
參考
KusionStack: https://github.com/KusionStack/kusion
KCL 配置語(yǔ)言編譯器: https://github.com/KusionStack/KCLVM
Rustc 官方文檔: https://rustc-dev-guide.rust-lang.org/
Rustc 源碼: https://github.com/rust-lang/rust
Rust Compiler Error Index: https://doc.rust-lang.org/error-index.html
?了解更多...
KusionStack Star 一下?:
https://github.com/KusionStack/Kusion

本文使用 文章同步助手 同步