Zig Sema:ZIR => AIR
本文為Zig 編譯器內部運作系列的一部分。
「AstGen」之後的下一個編譯階段是「Sema」。Sema 負責接收 AstGen 階段產生的 ZIR 輸出,並產生 AIR。AIR 是「Analyzed Intermediate Representation(已分析中間表示)」的縮寫,屬於完整具型別的中間表示,而 ZIR 則是無型別的中間表示。AIR 之後可直接降階為機器碼。
如 AstGen 篇所述,ZIR 為無型別的原因之一,在於要完整為 Zig 程式加上型別,必須進行 comptime 求值,才能讓泛型型別(以及其他要素)得以完整實現。因此,Sema 也會執行 Zig 程式中所有的 comptime 求值。這就是魔法發生的地方!
AIR 是以每個函式為單位產生,而非像 ZIR 或 AST 那樣以每個檔案為單位。本頁將聚焦於如何將函式本體從 ZIR 轉換為 AIR。未來的文章將說明更大範圍的編譯流程如何呼叫 Sema。
注意: 有一部分 AIR 是在檔案作用域中產生的,因此說 AIR 是按函式產生並不完全精確。然而,要理解檔案作用域 AIR 的流程,還需要更深入地討論編譯流程,這將留待未來的文章說明。本頁將作為邁向該理解的重要基石。
AIR 長什麼樣子?
在深入探討 AIR 的內部結構與建構方式之前,我們可以先看看範例 AIR。以下是一個簡單的 Zig 程式及其產生的 AIR:
export fn add(a: u32, b: u32) u32 {
return a + b;
}# Begin Function AIR: add:
%0 = arg("a", u32)
%1 = arg("b", u32)
%2!= dbg_stmt(2:5)
%3 = add(%0!, %1!)
%4!= ret(%3!)
# End Function AIR: add你可以透過執行 zig build-obj --verbose-air <file.zig> 來檢視任何程式的 AIR。這需要使用 Zig 編譯器的除錯版本(debug build)。Zig wiki 中有一份指南,說明如何從原始碼編譯 Zig 編譯器。
AIR 是以每個函式為單位產生與呈現。在上面的輸出中,你可以看到以註解形式標示 add 函式 AIR 輸出的行。另請注意,AIR 僅會為已匯出或被參照的函式產生(採延遲產生)。因此,為了除錯,我通常會將想檢視 AIR 的函式加上 export。
如果你已閱讀過編譯器先前階段的內容,會注意到 AIR 的呈現形式與 ZIR 非常相似。雖然兩者相似,且在許多情況下甚至共用指令標籤名稱,但 AIR 是完全獨立的中間表示。
%1 是指令的索引。當其後方加上 ! 時,表示該指令未被 Zig 程式中任何其他已知部分使用或參照。在上述範例中,%0 與 %1 皆用於加法指令以建構 %3,而 %3 又被 return 指令使用。但除錯陳述式 %2 與回傳結果 %4 則未被使用。將 AIR 轉換為最終格式的後端若有需要,可利用此資訊。
注意: 如前言所述,有一部分 AIR 是在檔案作用域而非函式作用域中產生的。例如,檔案作用域的變數初始化、comptime 區塊等。目前尚無法呈現這類 AIR。不過,檔案作用域的 AIR 通常只是一系列 constant 指令,因為它們一律會在 comptime 求值。
AIR 的結構
在探討 ZIR 如何轉換為 AIR 之前,我將先說明 AIR 的格式以及單一 AIR 指令的組成。理解 AIR 的結構,會讓理解 AIR 的建構過程變得容易許多。
AIR 的結構與 ZIR 非常相似,但有些微差異:
pub const AIR = struct {
instructions: std.MultiArrayList(Inst).Slice,
extra: []const u32,
values: []const Value,
};如同 ZIR,AIR 本質上就是儲存在 instructions 欄位中的一系列指令。而與 ZIR 和 AST 相似,指令也可能在 extra 切片中儲存指令專屬的額外資料。這兩個欄位的行為與 ZIR 和 AST 完全相同;如果你到目前為止仍未直觀理解它們,我建議回頭複習那些結構(特別是深入說明 extra 資料如何填入的 AST 結構)。
注意: 這裡我也略過對 MultiArrayList 的說明,因為這是與 ZIR 和 AST 相同的模式,已在剖析器(parser)探索中深入說明過。
AIR 中新增的欄位是 values 切片。這包含了來自 ZIR 的 comptime 執行結果的已知值。指令可能會參照 comptime 已知的值。例如,若檔案的根層級有 const a = 42;,則數值 42 會因屬於 comptime 已知而儲存在 values 清單中。稍後我們會看到更多 comptime 值的範例。
請注意,像是用於字串常數的字串表這類欄位並不存在。建構 AIR 的過程以及後續使用 AIR 的 Codegen 流程,仍可存取 ZIR,因此得以在 ZIR 的字串表中查詢資料。
單一 AIR 指令的結構
單一 AIR 指令的結構為 Inst 結構。
pub const Inst = struct {
tag: Tag,
data: Data,
};此結構在結構上與 ZIR 的 Inst 結構完全相同。其中有一個作為列舉的 tag,以及依標籤而異的 data,其為各種可能資料型別的聯合(union)。AIR 的 Tag 與 Data 型別雖然與 ZIR 的型別不同,但在功能上完全相同。
讓我們來看一個基本範例。當語意分析判定某個值為常數時,會建立 .constant 指令。標籤為 constant 的指令其資料為 ty_pl 欄位,其中包含常數的型別,而 payload 則是 values 陣列中該 comptime 已知值的索引。
const c = 42;%0 = constant(comptime_int, 42)Inst{
.tag = .constant,
.data = .{
.ty_pl = . {
.ty = Type.initTag(.comptime_int),
.pl = 7, // index into values
},
},
};上述範例展示了 Zig 程式碼、呈現後的 AIR,以及內部的指令表示。請注意,c 並未出現在任何地方,因為用來產生此 AIR 所分析的 ZIR 僅針對賦值右側(rhs)的常數 42。
Value、Type、TypedValue
在整個 Sema 中,有三種非常頻繁使用的型別:Value、Type 與 TypedValue。Value 代表一個 comptime 已知的數值,例如整數、結構等。Type 是 comptime 已知的型別,例如 u8(請注意:所有型別皆為 comptime 已知)。而 TypedValue 則是帶有精確已知型別的數值:也就是將數值 42 與型別 u16 配對起來。
有一個可能令人困惑之處在於,在 Zig 中型別本身也是合法的數值。一個型別可以是型別為「type」的值。舉例來說,在 Zig 中你可以寫 const c = u8。c 的型別為「type」,其值為「u8」。下方我們將展示許多此類情境的範例,以使其更直觀。
Value
Value 的結構如下所示:
pub const Value = extern union {
tag_if_small_enough: Tag,
ptr_otherwise: *Payload,
}一個型別具有描述其所屬值種類(kind)的 tag。我在此刻意使用「kind」而非「type」,因為 value 本身是無型別的,儘管在某些情況下其型別可被輕易推知。某些 tag 值沒有 payload,而其他則需要 payload。ptr_otherwise 欄位是指向 payload 的指標,其中包含取得該值所需的更多資訊。
Payload 型別是指向更具體 payload 型別中 Payload 型別欄位的指標。這是 Zig 中使用多型型別的方式之一。接著會使用 @fieldParentPtr 內建函式來判斷完整型別。這是 Zig 中常見的模式,更詳細的說明已超出本頁範圍。請搜尋 @fieldParentPtr 相關指南與 Zig 文件以了解其運作方式。
整數
讓我們來看看整數值。常數 42 以下方的 Value 表示:
Value{
.ptr_otherwise = &Payload.U64{
.tag = .int_u64,
.data = 42,
},
};注意: 該值並非完全正確。ptr_otherwise 欄位指向的是 Payload,而非完整的 Payload.U64 結構。不過,上方的寫法更能清楚表達意圖,因此本文將全程採用此格式。
如我們所見,數值 42 是以 int_u64 旗標來表示。這是因為 int_u64 用於表示所有可容納於 u64 範圍內的值。它並不意味著 42 就是 u64 型別——它仍可能是 u8、u16 等。沒有 Type 的 Value 是無型別的。或者,若你對「我們似乎已有某種型別」感到困擾,也可以將 value 視為並非精確具型別的。
Type Value(非 TypedValue)
在 Zig 中,型別也可以是一個值。例如,陳述式 const c = u8 是完全合法的 Zig:你將型別 u8 指派給常數 c,而 c 本身的型別為 type(它是一個持有型別的值)。這可能非常令人困惑,而且隨著型別值在實際語意分析中的使用,會變得更加混淆,因此在此先行說明。
作為值的常數 u8 以下方的 Value 表示:
Value{
.tag_if_small_enough = .u8_type,
};此值沒有 payload。該值完全由標籤 u8_type 表示。此值即為 u8 型別。但該值本身仍是無型別的。不過,在此情況下其型別可被輕易推知,因為 u8_type 唯一合法的型別就是 type。但 Value 結構本身在技術上仍是無型別的(例如,未來 Zig 可能會引入關鍵字 inttype 來表示所有整數型別,屆時此值合法的型別可能是 inttype或 type)。
讓我們來看一個更複雜的型別值:const c = [4]bool。
Value{
.ptr_otherwise = &Payload.Ty{
.tag = .ty,
.data = Type{
// .tag = .array
// type data including element type (bool), length (4), etc.
},
},
};此值的標籤為 .ty(即「type」的縮寫)。它是一個其值為某個型別的值。其 payload 是描述包含 4 個布林元素的陣列的 Type 結構。我們將在下一節更詳細地說明 Type 結構。這裡的重點在於,Value 所表示的是陣列型別值,而非陣列值。
若仍有困惑,請查看為 Type 定義的 toValue 函式。一個 Type 永遠可以轉換為 value,因為型別可以是值,但反之則不一定成立。
Type
Type 結構與 Value 等價,只是使用型別專屬的型別:
pub const Type = extern union {
tag_if_small_enough: Tag,
ptr_otherwise: *Payload,
}這裡沒有太多需要補充的,因為其行為與 Value 幾乎完全相同。讓我們看看 [4]bool 如何表示,以便至少有一個具體範例:
Type{
.ptr_otherwise = &Payload.Array{
.tag = .array,
.data = .{
.len = 4,
.elem_type = Type{.tag_if_small_enough = .bool},
},
}
}TypedValue
TypedValue 僅是將 Type 與 Value 結合在一起,使數值與精確的型別資訊配對。唯有兩者兼具,才能確知一個值的精確型別。
pub const TypedValue = struct {
ty: Type,
val: Value,
};Sema 的結構
Zig 編譯器中「AstGen」之後的下一個階段,口語上稱為「Sema」。Sema 也是主要負責此階段的結構。Sema 的原始碼位於 src/Sema.zig。這是一個非常大的檔案(撰寫本文時已超過 18,000 行),並自稱為「Zig 編譯器的核心」。
Sema 有許多公開 API,但最重要的是 analyzeBody。Sema 結構有許多用於內部狀態的欄位。我們不會逐一說明,但下方展示其中一些主要的欄位。為便於說明相似欄位,這些欄位的順序與原始碼中的順序不同。
pub const Sema = struct {
mod: *Module,
gpa: Allocator,
arena: Allocator,
perm_arena: Allocator,
code: Zir,
owner_decl: *Decl,
func: ?*Module.Fn,
fn_ret_ty: Type,
air_instructions: std.MultiArrayList(Air.Inst) = .{},
air_extra: std.ArrayListUnmanaged(u32) = .{},
air_values: std.ArrayListUnmanaged(Value) = .{},
inst_map: InstMap = .{},
// other fields...
};第一組是 Sema 流程所需的主要輸入。gpa 用於配置在 Sema 流程與宣告生命週期之外仍需存活的資料。arena 用於配置在 Sema 結束後即釋放的暫時性資料。而 perm_arena 則用於配置與正在進行語意分析的宣告生命週期綁定的資料。
mod 是正在被分析的模組。本頁不會涵蓋模組,但它封裝了單一程式中的所有 Zig 程式碼。在本頁中我們將忽略所有模組 API。
第二組是描述 Sema 正在進行語意分析對象的輸入。code 是包含正被分析之宣告所在檔案的 ZIR。owner_decl 通常是目前正被分析的宣告,例如函式、comptime 區塊、測試等。func 與 fn_ret_ty 則是在分析函式時的額外資訊。
第三組是 Sema 流程的輸出。你可能會注意到,這些大多是構成 Air 結構的欄位。它們會在整個 Sema 過程中被填入,並用於建構最終的 Air 結果。
inst_map 欄位特別重要,並在整個 Sema 中被廣泛使用。這是一個從 ZIR 對應至 AIR 的映射。並非所有 ZIR 指令都會產生 AIR 指令,但此映射會被頻繁使用,讓 AIR 指令得以參照稍後才被解析的特定 ZIR 指令(例如加法運算元、函式參數等)。
分析函式本體
Sema 中的核心函式是 analyzeBody。它用於分析某個「本體(body)」——函式本體、迴圈本體、區塊本體等——的 ZIR,並為該本體產生 AIR。最簡單的本體是函式本體。首先,讓我們看看一個非常簡單的函式及其產生的 AIR:
export fn add() u32 {
return 40 + 2;
}# Begin Function AIR: add:
%1 = constant(comptime_int, 40)
%2 = constant(comptime_int, 2)
%3 = constant(comptime_int, 42)
%4 = constant(u32, 42)
%0!= dbg_stmt(2:5)
%5!= ret(%4!)
# End Function AIR: add提醒: 我們可以透過將函式匯出並執行 zig build-obj --verbose-air example.zig 來傾印該函式的 AIR。
即使不了解 AIR 指令的具體細節,你也應該能夠推斷發生了什麼事。下方列出大致的步驟流程:
- 我們看到無型別的常數整數
40(指令%1) - 我們看到無型別的常數整數
2(指令%2) - 我們在 comptime 執行加法,並得知結果為無型別常數整數
42(指令%3)。 - 常數
42與型別u32配對(指令%4)。由於數值42可自動容納於u32型別,無需進行轉換。此定型是為了符合回傳型別u32所必需。 - 我們回傳
%4所產生的值(指令%5)。
很簡潔吧!這裡有些冗餘,但可以清楚看到它如何實現我們的 add 函式。此外,你可以看到 comptime 求值已在此階段完成,加法運算也已在此完成,因此結果已預先得知。當最終被降階為機器碼時,實際的加法結果已經預先計算完成。
接下來,讓我們逐步看看此 AIR 是如何產生的。
逐步走訪函式
主要的 AIR 產生迴圈位於 analyzeBodyInner(由 analyzeBody 呼叫)。它依序遍歷 ZIR 指令,並為每個單獨的 ZIR 指令產生零或多個 AIR 指令。
const result = while (true) {
const inst = body[i];
const air_inst: Air.Inst.Ref = switch (tags[inst]) {
.alloc => try sema.zirAlloc(block, inst),
.alloc_inferred => try sema.zirAllocInferred(block, inst, Type.initTag(.inferred_alloc_const)),
.alloc_inferred_mut => try sema.zirAllocInferred(block, inst, Type.initTag(.inferred_alloc_mut)),
.alloc_inferred_comptime => try sema.zirAllocInferredComptime(inst, Type.initTag(.inferred_alloc_const)),
.alloc_inferred_comptime_mut => try sema.zirAllocInferredComptime(inst, Type.initTag(.inferred_alloc_mut)),
.alloc_mut => try sema.zirAllocMut(block, inst),
// hundreds more...
};
try sema.inst_map.put(sema.gpa, inst, air_inst);
i += 1;
}這使得理解針對給定 ZIR 如何產生 AIR 變得非常容易。你可以傾印 ZIR,並逐一檢視每個指令來判斷 AIR 是如何產生的。上述範例中 add 函式的 ZIR 如下:
%0 = extended(struct_decl(parent, Auto, {
[25] export add line(6) hash(4ca8d4e33898374bdeee80480f698dad): %1 = block_inline({
%10 = func(ret_ty={
%2 = break_inline(%10, @Ref.u32_type)
}, body={
%3 = dbg_stmt(2, 5)
%4 = extended(ret_type()) node_offset:8:5
%5 = int(40)
%6 = int(2)
%7 = add(%5, %6) node_offset:8:15
%8 = as_node(%4, %7) node_offset:8:15
%9 = ret_node(%8) node_offset:8:5
}) (lbrace=1:21,rbrace=3:1) node_offset:7:8
%11 = break_inline(%1, %10)
}) node_offset:7:8
}, {}, {})函式的本體始於 ZIR 指令 %3,止於 %9。%3 是 analyzeBodyInner 在分析 add 函式本體時看到的第一個指令。較早(與較晚)的 ZIR 指令,例如 %2 與 %10,會更早被分析;該流程稍後會再討論。
%3:dbg_stmt
第一個指令是 .dbg_stmt。在主迴圈中可以看到它會導向 zirDbgStmt。該函式以稍微簡化的形式重現如下:
fn zirDbgStmt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
const inst_data = sema.code.instructions.items(.data)[inst].dbg_stmt;
_ = try block.addInst(.{
.tag = .dbg_stmt,
.data = .{ .dbg_stmt = .{
.line = inst_data.line,
.column = inst_data.column,
} },
});
}這是 AIR 指令由 ZIR 轉譯而來的良好簡易範例。沒有比這更簡單的了。在此情況下,dbg_stmt ZIR 幾乎被一對一地轉譯為 dbg_stmt AIR 指令。這產生了先前所示的 AIR 指令 %0:
%0!= dbg_stmt(2:5)%4:extended(ret_type())
.extended ZIR 指令會呼叫 zirExtended,其會遍歷子操作碼以將 .ret_type 對應至 zirRetType。此函式重現如下:
fn zirRetType(
sema: *Sema,
block: *Block,
extended: Zir.Inst.Extended.InstData,
) CompileError!Air.Inst.Ref {
const src: LazySrcLoc = .{ .node_offset = @bitCast(i32, extended.operand) };
try sema.requireFunctionBlock(block, src);
return sema.addType(sema.fn_ret_ty);
}正被分析之函式的回傳型別可透過 Sema 上的 fn_ret_ty 欄位取得。addType 函式會為型別定義新增一個指令。對於我們的函式,其結果型別為 u32,屬於廣為人知的型別,因此不會產生額外的指令。
作為實驗,若將回傳值改為 u9——一個非廣為人知的型別——AIR 會產生以下指令:
%5 = const_ty(u9)%5:int(40)
接下來是我們常數 40 的 .int 指令。這會在主要的分析本體迴圈中導向 zirInt 函式呼叫。zirInt 函式會讀取整數值並呼叫 addConstant。兩個函式如下所示。
fn zirInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
const int = sema.code.instructions.items(.data)[inst].int;
return sema.addConstant(ty, try Value.Tag.int_u64.create(sema.arena, int));
}pub fn addConstant(sema: *Sema, ty: Type, val: Value) SemaError!Air.Inst.Ref {
const gpa = sema.gpa;
const ty_inst = try sema.addType(ty);
try sema.air_values.append(gpa, val);
try sema.air_instructions.append(gpa, .{
.tag = .constant,
.data = .{ .ty_pl = .{
.ty = ty_inst,
.payload = @intCast(u32, sema.air_values.items.len - 1),
} },
});
return Air.indexToRef(@intCast(u32, sema.air_instructions.len - 1));
}addConstant 函式在 Sema 中的許多地方被呼叫,用於新增 comptime 已知的值。它首先使用 addType 新增型別,這一點我們在上一個指令中已見過。在此情況下,型別為 comptime_int,屬於廣為人知的型別,因此不會產生新指令。接著,該值會被加入 air_values,最後 constant AIR 指令會被加入 air_instructions。這會產生以下 AIR 指令:
%1 = constant(comptime_int, 40)有一點需要注意,.constant 指令的 payload 參照的是 air_values 切片中的索引。所有 comptime 已知的值皆儲存在 air_values 中,而指令中的任何參照皆以 air_values 切片索引的形式儲存。
我們將略過 %6,因為它與 %5 相同,只是常數為 2。
%7:add(%5, %6)
接下來,我們迎來第一個真正的邏輯運算:加法。.add 指令會導向 zirArithmetic 函式。此函式用於許多二元數學運算。該函式如下所示:
fn zirArithmetic(
sema: *Sema,
block: *Block,
inst: Zir.Inst.Index,
zir_tag: Zir.Inst.Tag,
) CompileError!Air.Inst.Ref {
const inst_data = sema.code.instructions.items(.data)[inst].pl_node;
sema.src = .{ .node_offset_bin_op = inst_data.src_node };
const lhs_src: LazySrcLoc = .{ .node_offset_bin_lhs = inst_data.src_node };
const rhs_src: LazySrcLoc = .{ .node_offset_bin_rhs = inst_data.src_node };
const extra = sema.code.extraData(Zir.Inst.Bin, inst_data.payload_index).data;
const lhs = sema.resolveInst(extra.lhs);
const rhs = sema.resolveInst(extra.rhs);
return sema.analyzeArithmetic(block, zir_tag, lhs, rhs, sema.src, lhs_src, rhs_src);
}頂端的所有常數變數賦值都是在解碼 ZIR 指令。其中特別重要的賦值為 lhs 與 rhs。resolveInst 函式用於為給定的 ZIR 指令尋找對應的 AIR 指令索引。因此,給定 ZIR 指令 %5 與 %6,lhs 與 rhs 分別被設為其對應的 AIR 索引。這些 AIR 指令是在先前產生參數之 .constant 指令的迴圈迭代中所設定的。
ZIR ⇒ AIR 的對應關係維護於何處? ZIR 至 AIR 的對應關係由 analyzeBodyInner 迴圈維護於 inst_map 欄位中。雖然還有其他少數地方可能會更新它,但本體迴圈是主要位置。
接著會導向 analyzeArithmetic。這是一個較長的函式,其絕大多數程式碼都在判斷是否能對此算術運算進行 comptime 分析。關鍵的程式碼行重現如下:
const maybe_lhs_val = try sema.resolveMaybeUndefVal(block, lhs_src, casted_lhs);
const maybe_rhs_val = try sema.resolveMaybeUndefVal(block, rhs_src, casted_rhs);
if (maybe_lhs_val) |lhs_val| {
if (maybe_rhs_val) |rhs_val| {
if (is_int) {
return sema.addConstant(
scalar_type,
try lhs_val.intAdd(rhs_val, sema.arena),
);
}
}
}
return block.addBinOp(.add, casted_lhs, casted_rhs);它首先呼叫 resolveMaybeUndefVal。此函式接受一個 AIR 指令索引,並嘗試載入其對應的 comptime 已知值。這會回傳一個 optional,因為若該值無法在 comptime 得知,回傳值將為 null。
接著,我們嘗試解開 optional。若我們能為 lhs 與 rhs 兩者皆找到 comptime 已知的值,且兩者皆為整數型別,則可在 comptime 執行加法以產生最終常數。這正是我們的程式發生之事,進而產生帶有結果 42 的 .constant AIR 指令:
%3 = constant(comptime_int, 42)我也包含了當值並非 comptime 已知時的 fallthrough:block.addBinOp。這會新增一個用於執行時期計算(而非 comptime)的 .add AIR 指令。作為實驗:可將常數 2 改為變數,例如 var b: u32 = 2,並將其用於加法。這會產生 .add 運算,因為變數無法在 comptime 中運算。
%8:as_node(%4, %7)
下一個指令實現了安全的型別強制轉換(type coercion)。觀察 ZIR,這是將加法結果轉換為回傳型別。更具體地以我們已知的情況來說:我們必須將 comptime 整數 42 轉換為 u32。
.as_node 指令會呼叫 zirAsNode,最終導向 coerce 中的核心邏輯。coerce 函式在整個 Sema 中被用於執行從一個型別到另一個型別的安全型別強制轉換。
此函式的研讀就留給讀者自行探索。其邏輯相當直觀,但因需處理多種型別強制轉換情況而顯得冗長。對於從 comptime_int 到 u32 的情況,會判定該值可容納於 u32 中,並按原樣回傳該值。型別轉換無需額外的處理。
%9:ret_node(%8)
最後,return 指令在 ZIR 中被編碼為 ret_node。這會導向 zirRetNode,其會建立帶有結果的 .ret AIR 指令。此指令的建立沒有任何新穎或尚未探討之處。
zirRetNode 函式會回傳 always_noreturn。此值會迫使 analyzeBodyInner 迴圈退出,從而完成函式本體的 AIR 產生。
return 總是會完成函式本體的 AIR 產生嗎?那多個 return 陳述式呢? return 總是會完成當前區塊的 AIR 產生。不可達程式碼是不合法的,並會在 AstGen 期間被捕捉,這意味著多個 return 陳述式唯一可能的方式是位於不同的區塊中。因此,在函式的脈絡下,多個 return 陳述式是沒有問題的,因為它會遞迴進入多個 analyzeBodyInner 呼叫。
無法使用 Comptime
我們查看的第一個範例有點乏味,因為所有邏輯皆可在 comptime 中處理。Comptime 僅適用於 const 值,而不適用於 var 值,因此我們可以透過使用 var 來查看執行時期加法的 AIR:
export fn add() u32 {
var b: u32 = 2;
return 40 + b;
}# Begin Function AIR: add:
%2 = constant(comptime_int, 2)
%3 = constant(u32, 2)
%6 = constant(comptime_int, 40)
%8 = constant(u32, 40)
%0!= dbg_stmt(2:5)
%1 = alloc(*u32)
%4!= store(%1, %3!)
%5!= dbg_stmt(3:5)
%7 = load(u32, %1!)
%9 = add(%8!, %7!)
%10!= ret(%9!)
# End Function AIR: add將常數 2 改為以變數賦值的 2 會產生多得多的 AIR!我們現在有了透過 .alloc 的配置、透過 .store 的數值儲存,你也可以看到執行時期的 .add 運算。
這是一個適合研讀與追蹤以學習 AIR 如何產生的良好程式。由於它遵循與先前範例相同的許多程式碼路徑,我將其留作讀者的練習。
Comptime 目標模擬
對於在 comptime 求值的程式碼,Zig 編譯器會在必要時模擬目標平台的特性。這是一項關鍵功能,使 Zig 中的 comptime 求值得以安全且可行。
此特性的一個範例可在 @floatCast 的實作中看到:
const target = sema.mod.getTarget();
const src_bits = operand_ty.floatBits(target);
const dst_bits = dest_ty.floatBits(target);
if (dst_bits >= src_bits) {
return sema.coerce(block, dest_ty, operand, operand_src);
}
return block.addTyOp(.fptrunc, dest_ty, operand);該函式透過 getTarget 取得關於目標的資訊,然後判斷浮點數在目標平台上可支援的位元數。若目的地的位元數多於來源,則可安全地強制轉換型別。否則,浮點數會被截斷。
完成 Sema 流程
Sema 每個函式會被呼叫一次。這是首次以每個函式而非每個檔案為單位執行的流程。Sema 並非遍歷每個宣告,而是僅針對每個被參照的宣告各呼叫一次。為確定所有被參照的宣告,Sema 會從處理程序的進入點函式開始。
這實現了 Zig 的「延遲分析(lazy analysis)」。這意味著某些錯誤除非該宣告被參照,否則不會產生編譯器錯誤。這使得編譯過程極為快速,且由於僅編譯被參照的程式碼,產生的 codegen 也更小,但確實有時會導致令人困惑的行為。
具備本頁所說明的基礎知識後,你應該能夠追蹤任何 Zig 程式並判斷其如何被轉譯為 AIR。請記得經常使用 zig ast-check 與 zig build-obj --verbose-air 指令來檢視 Zig 編譯器所產生的內容。
在此之後,AIR 會被交給「codegen」流程以降階為最終格式。Codegen 是共用編譯器「前端」與多個「後端」之間的邊界。後端可能是 LLVM,也可能是諸如 WASM 的原生後端。
隨機一篇部落格