Zig Sema:ZIR => AIR
原文由 Mitchell Hashimoto 于 发布,订阅该博客
本文是Zig 编译器内部原理系列的一部分。
“AstGen”之后的下一个编译阶段是“Sema”。Sema 负责接收 AstGen 阶段产生的 ZIR 并生成 AIR。AIR 全称为“已分析中间表示(Analyzed Intermediate Representation)”,是一种完全带类型的中间表示,而 ZIR 则是无类型的中间表示。AIR 随后可直接降低为机器码。
正如在 AstGen 一文中提到的,ZIR 无类型的原因之一在于,要完整地为 Zig 程序确定类型,需要进行编译期求值,以便实例化泛型等。因此,Sema 同时也负责执行 Zig 程序的所有编译期求值。这就是奇迹发生的地方!
AIR 按函数生成,而不像 ZIR 或 AST 那样按文件生成。本页将重点介绍如何将函数体从 ZIR 转换为 AIR。后续文章将讨论更宏观的编译流程如何调用 Sema。
注意:也有一些 AIR 是在文件作用域生成的,因此说 AIR 完全按函数生成并不 100% 准确。不过,要理解文件作用域 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 构建版本。Zig Wiki 上有从源码编译 Zig 编译器的指南。
AIR 按函数生成和渲染。在上面的输出中,你可以看到标注 add 函数 AIR 输出的注释行。还要注意,AIR 仅为已导出或被引用的函数生成(它是惰性生成的)。因此,出于调试目的,我通常会把想查看 AIR 的函数加上 export。
如果你一直在关注编译器的前几个阶段,会发现 AIR 的文本形式与 ZIR 非常相似。虽然它们相似,甚至在许多情况下共享相同的指令标签名,但 AIR 是一个完全独立的中间表示。
%1 是指令的索引。当它后面跟着 ! 时,表示该指令未被 Zig 程序中任何其他已知部分使用或引用。在上面的例子中,%0 和 %1 都被用于加法指令来构造 %3,而 %3 又被返回指令使用。但调试语句 %2 和返回结果 %4 未被使用。将 AIR 转换为最终格式的后端如果觉得有用,可以利用这一信息。
注意:如前言所述,也有一些 AIR 是在文件作用域而非函数作用域生成的。例如,文件作用域的变量初始化、编译期块等。目前还无法渲染这些 AIR。不过文件作用域的 AIR 通常只是一系列 constant 指令,因为它们始终在编译期求值。
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 完全一致;如果你到现在还没有直观理解它们,建议回头复习一下相关结构(尤其是 AST 结构,那里深入讲解了 extra 数据是如何填充的)。
注意:这里我也略过了对 MultiArrayList 的解释,因为这与 ZIR 和 AST 中使用的模式完全相同,已在解析器探索一文中详细说明过。
AIR 中新增的字段是 values 切片。它包含了对 ZIR 进行编译期执行后得到的已知值。指令可以引用编译期已知的值。例如,如果文件顶层有 const a = 42;,那么值 42 就会被存储在 values 列表中,因为它是编译期已知的。后面我们会看到更多编译期值的例子。
注意,这里并没有出现用于字符串常量的字符串表等字段。AIR 的构建过程以及后续使用 AIR 的 Codegen 过程仍然可以访问 ZIR,从而在 ZIR 的字符串表中查找数据。
单条 AIR 指令的结构
单条 AIR 指令的结构是 Inst 结构体。
pub const Inst = struct {
tag: Tag,
data: Data,
};该结构体在结构上与 ZIR 的 Inst 结构体完全相同。有一个作为枚举的 tag,以及与标签相关的 data,后者是多种可能数据类型的联合。AIR 的 Tag 和 Data 类型与 ZIR 的类型不同,但在功能上完全一致。
我们来看一个基本示例。当语义分析确定某个值是常量时,它会创建 .constant 指令。constant 标签指令的数据是 ty_pl 字段,其中包含常量的类型,payload 则是 values 数组中编译期已知值的索引。
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 表示编译期已知的值,例如整数、结构体等。Type 是编译期已知的类型,例如 u8(注意:所有类型都是编译期已知的)。而 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,
}一个值有一个描述其种类的 tag。这里刻意使用“种类”而不是“类型”,因为 Value 本身是无类型的,尽管在某些情况下其类型可以被轻易推断出来。有些 tag 值没有 payload,而另一些则需要 payload。ptr_otherwise 字段是指向包含获取该值所需更多信息的 payload 的指针。
Payload 类型是指向更具体 payload 类型的 Payload 类型字段的指针。这是 Zig 中实现多态类型的一种方式。随后会使用 @fieldParentPtr 内建函数来确定完整类型。这是 Zig 中常见的模式,在此不再进一步展开。请自行搜索关于 @fieldParentPtr 的指南来了解其工作原理。
整数
我们来看看整数值。常量 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 是无类型的。或者,如果这让你感到困扰,你也可以把值看作是“尚未精确确定类型”。
类型值(而非 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 总是可以被转换为值,因为类型可以是值,但反之则不一定成立。
类型
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 通常是当前正在分析的声明,例如函数、编译期块、测试等。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) - 我们在编译期执行加法,得知结果是无类型的常量整数
42(指令%3)。 - 常量
42与类型u32配对(指令%4)。由于值42可以自动放入u32类型,无需进行转换。这一步定类型是为了匹配返回类型u32。 - 我们返回在
%4中产生的值(指令%5)。
不错!这里有些冗余,但可以清楚地看到它是如何实现 add 函数的。另外,你也可以看到编译期求值已经完成,加法结果已被预先计算出来。当它最终被翻译为机器码时,实际的加法结果已经是预先算好的。
接下来,我们一步步看看这段 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,
} },
});
}这是一个将 ZIR 指令翻译为 AIR 指令的很好且最简单的例子。再没有比这更简单的了。在这种情况下,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 的许多地方被调用,用于添加编译期已知的值。它首先使用 addType 添加类型,这一点我们在上一条指令中已经见过。在这种情况下,类型是 comptime_int,这是一个广为人知的类型,不会产生新指令。接着,值被添加到 air_values 中,最后 constant 这一 AIR 指令被添加到 air_instructions 中。这产生了如下 AIR 指令:
%1 = constant(comptime_int, 40)需要注意的是,.constant 指令的 payload 引用的是 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。这是一个很长的函数,几乎所有代码都在判断是否可以对该算术运算进行编译期分析。关键代码如下所示:
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 指令索引并尝试加载其编译期已知的值。它返回一个可选值,因为如果该值无法在编译期获知,返回值将为 null。
接下来,我们尝试解包这些可选值。如果我们能够为 lhs 和 rhs 都找到编译期已知的值,并且它们都是整数类型,那么我们就可以通过编译期加法产生最终的常量。我们的程序正是这样产生了带有结果 42 的 .constant AIR 指令:
%3 = constant(comptime_int, 42)我也包含了当值在编译期未知时的回退路径:block.addBinOp。这会添加一条用于运行时计算的 .add AIR 指令(而非编译期)。要做实验:可以将常量 2 改为变量,例如 var b: u32 = 2 并用它进行加法。这将产生一个 .add 操作,因为变量无法在编译期进行运算。
%8: as_node(%4, %7)
下一条指令实现了安全的类型强制转换。观察 ZIR,这是在将加法结果转换为返回类型。更具体地说,就我们已知的而言:我们必须将编译期整数 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 语句的唯一可能是它们位于不同的块中。因此,在函数的上下文中,多个返回语句是完全正常的,因为它会递归进入多次 analyzeBodyInner 调用。
无法在编译期求值
我们之前看的第一个例子有点无趣,因为所有逻辑都可以在编译期完成。编译期求值仅对 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 如何生成的好程序。我把它作为留给读者的练习,因为它遵循了与我们之前例子中许多相同的代码路径。
编译期目标平台仿真
对于编译期求值的代码,Zig 编译器会在必要时仿真目标平台的特性。这是一项关键功能,使得 Zig 中的编译期求值既安全又可行。
在 @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 的“惰性分析”。这意味着,除非声明被引用,否则某些错误不会产生编译器错误。这使得编译过程极快,生成的代码也更小,因为只有被引用的代码才会被编译,但它有时也会导致一些令人困惑的行为。
掌握本页所讲解的基础知识后,你应该能够跟踪任意 Zig 程序并判断它是如何被翻译为 AIR 的。请记得经常使用 zig ast-check 和 zig build-obj --verbose-air 命令来查看 Zig 编译器生成的内容。
在此之后,AIR 会被移交给“codegen”过程,以将其降低为最终格式。Codegen 是共享编译器“前端”与多个“后端”之间的边界。后端可以是 LLVM,也可以是诸如 WASM 之类的原生后端。
随机一篇博客
评论
登录后参与讨论