Zig Sema: ZIR => AIR
この記事はZigコンパイラの内部構造に関するシリーズの一部です。
「AstGen」の次のコンパイラステージが「Sema」です。Semaは、AstGenステージが出力したZIRを受け取り、AIRを生成する役割を担います。AIRは「Analyzed Intermediate Representation(解析済み中間表現)」の略で、完全に型付けされた中間表現です。一方、ZIRは型付けされていない中間表現です。AIRはその後、直接機械語へとローワリングできます。
AstGenのページでも触れたとおり、ZIRが型なしである理由の一つは、Zigプログラムを完全に型付けするにはcomptime評価が必要であり、そうすることでジェネリック型などが完全に実体化されるためです。したがって、SemaはZigプログラムのすべてのcomptime評価も担います。まさにここで魔法が起きるのです!
ZIRやASTがファイル単位で生成されるのに対し、AIRは関数単位で生成されます。本ページでは、関数本体を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任意のプログラムのAIRは、zig build-obj --verbose-air <file.zig>を実行することで確認できます。これにはZigコンパイラのデバッグビルドが必要です。Zigコンパイラをソースからビルドする方法については、Zig Wikiにガイドがあります。
AIRは関数単位で生成され、表示されます。先ほどの出力では、add関数のAIR出力を示すコメント風の行が確認できます。また、AIRはexportされた関数や参照されている関数に対してのみ生成されることにも注意してください(遅延生成されます)。そのため、デバッグの際は、AIRを確認したい関数に通常exportを付けるようにしています。
コンパイラのこれまでのステージについての解説を読んでいれば、AIRの表示形式がZIRと非常によく似ていることに気づくでしょう。両者は似ており、場合によっては命令のタグ名を共有することさえありますが、AIRは完全に独立した中間表現です。
%1は命令のインデックスを表します。その後に!が付いている場合は、その命令がZigプログラムの他の既知の部分から使われても参照されてもいないことを意味します。先ほどの例では、%0と%1はともに加算命令で使われて%3を構成し、%3はreturn命令で使われています。一方、デバッグ文の%2とreturn結果の%4は未使用です。AIRを最終的な形式に変換するバックエンドは、必要に応じてこの情報を利用できます。
注記: 冒頭でも触れたとおり、一部のAIRは関数スコープではなくファイルスコープで生成されます。例えば、ファイルスコープでの変数初期化やcomptimeブロックなどです。現在、このAIRを表示することはできません。ただし、ファイルスコープのAIRは常にcomptime評価されるため、通常は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スライスに命令固有の追加データを格納することがあります。これら2つのフィールドの動作はZIRやASTとまったく同じです。もし現時点で直感的に理解できない場合は、これらの構造(特にextraデータの格納方法を詳しく解説しているASTの構造)に立ち戻って復習することをおすすめします。
注記: MultiArrayListについての説明もここでは省略します。これはZIRやASTでも使われているまったく同じパターンであり、パーサーの解説で詳しく説明済みだからです。
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というenumと、タグ固有のdataという取り得るデータ型のunionがあります。AIRのTag型やData型はZIRのものとは別物ですが、機能的には同一です。
基本的な例を見てみましょう。意味解析によってある値が定数であると判断されると、.constant命令が生成されます。constantタグの命令のデータはty_plフィールドで、定数の型を含み、ペイロードはcomptimeで既知の値が格納された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全体で非常によく使われる3つの型があります。Value、Type、そしてTypedValueです。Valueは整数や構造体などのcomptimeで既知の値を表します。Typeはu8のようなcomptimeで既知の型を表します(なお、すべての型は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,
}型(type)は、値がどのような種類(kind)であるかを表すtagを持ちます。ここで意図的に「type」ではなく「kind」という言葉を使っているのは、値自体は型付けされていないものの、場合によっては型が自明に分かることがあるためです。tagの値の中にはペイロードを持たないものもあれば、ペイロードを必要とするものもあります。ptr_otherwiseフィールドは、値を取得するために必要な追加情報を含むペイロードへのポインタです。
Payload型は、より具体的なペイロード型の中にあるPayload型のフィールドへのポインタです。これはZig内部でポリモーフィックな型が使われる方法の一つです。そして@fieldParentPtr組み込み関数を使って完全な型が特定されます。これはZigでよく見られるパターンであり、詳細な説明は本ページの範囲外です。仕組みを理解するには、@fieldParentPtrに関するガイドを検索してみてください。
整数
整数値を見てみましょう。定数42は以下のValueで表現されます。
Value{
.ptr_otherwise = &Payload.U64{
.tag = .int_u64,
.data = 42,
},
};注記: この値は厳密には正確ではありません。ptr_otherwiseフィールドは完全なPayload.U64構造体ではなくPayloadを指します。ただし、上記の方が意図が明確なので、本記事ではこの形式で統一します。
ご覧のとおり、値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,
};この値はペイロードを持ちません。値はタグ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」の略)というタグを持ちます。これは何らかの型である値です。ペイロードは、4つのboolean要素からなる配列を記述するType構造体です。Type構造体については次のセクションでより詳しく説明します。ここで重要なのは、Valueが表しているのは配列の値ではなく、配列の型の値だという点です。
まだ混乱がある場合は、Typeに対して定義されているtoValue関数を見てみてください。Typeは常に値に変換できます。型は値になり得るからです。しかし、その逆は必ずしも成り立ちません。
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は無視します。
2つ目のグループは、Semaが意味解析している対象を記述する入力です。codeは、解析中のdeclを含むファイルのZIRです。owner_declは通常、現在解析中の宣言で、関数やcomptimeブロック、テストなどです。funcとfn_ret_tyは、関数を解析する際の追加情報です。
3つ目のグループは、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補足: 関数のAIRは、その関数をexportしてzig build-obj --verbose-air example.zigを実行することでダンプできます。
AIR命令の詳細を知らなくても、何が起きているかは大まかに把握できるはずです。大まかな流れを以下に示します。
- 型なしの定数整数
40が現れます(命令%1) - 型なしの定数整数
2が現れます(命令%2) - 加算をcomptimeで実行し、結果が型なしの定数整数
42であることが分かります(命令%3) - 定数
42が型u32と紐づけられます(命令%4)。値42は自動的にu32に収まるため、変換は不要です。この型付けは、戻り値の型u32に合わせるために必要です。 %4で生成された値を返します(命令%5)
いい感じですね!少し冗長ではありますが、この関数addがどのように実装されているかは明確に分かります。また、comptime評価が実行され、この段階で加算が済んでいるため、結果はすでに事前に分かっていることも確認できます。これが最終的に機械語へと翻訳される際には、実際の加算結果はすでに事前計算済みなのです。
次に、このAIRがどのように段階を追って生成されたのかを見ていきましょう。
関数をステップごとに追う
AIR生成のメインループはanalyzeBodyInner(analyzeBodyから呼ばれます)にあります。これはZIR命令を順に反復し、個々のZIR命令ごとに0個以上の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をダンプして1命令ずつ追うことで、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関数の本体を解析する際に最初に目にする命令です。%2や%10のようなそれ以前(およびそれ以降)のZIR命令は、より早い段階で解析されます。そのプロセスについては後ほど少し触れます。
%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命令へと翻訳されます。これにより、先ほど示した%0のAIR命令が生成されます。
%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関数は、comptimeで既知の値を追加するためにSemaの多くの場所で呼ばれます。まずaddTypeを使って型を追加しますが、これは前の命令でも見ました。今回の場合、型はcomptime_intであり、これはよく知られた型なので新しい命令は生成されません。次に、値がair_valuesに追加され、最後にconstantのAIR命令がair_instructionsに追加されます。これにより、次のAIR命令が生成されます。
%1 = constant(comptime_int, 40)注意すべき点として、.constant命令のペイロードはair_valuesスライスへのインデックスを参照していることが挙げられます。すべてのcomptimeで既知の値はair_valuesに格納され、命令内の参照はすべてair_valuesスライスへのインデックスとして格納されます。
%6は定数2に対するもので%5と同一なので、説明を省略します。
%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で既知の値を読み込もうとします。値がcomptimeで既知でない場合は戻り値がnullになるため、optionalが返されます。
次に、optionalのアンラップを試みます。lhsとrhsの両方でcomptimeで既知の値が見つかり、かつ両方が整数型であれば、comptimeでの加算を行って最終的な定数を生成できます。今回のプログラムで、結果42を持つ.constantのAIR命令が生成されるのは、この処理によるものです。
%3 = constant(comptime_int, 42)値がcomptimeで既知でない場合のフォールスルーとしてblock.addBinOpも含めました。これは(comptimeではなく)ランタイム計算のために.addのAIR命令を追加します。試してみるには、定数の2をvar b: u32 = 2のような変数に変え、それを加算に使ってみてください。変数はcomptimeで演算できないため、これは.add演算を生成します。
%8: as_node(%4, %7)
次の命令は、安全な型強制を実装します。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文が存在する唯一の方法は、それらが異なるブロックにあることです。したがって、関数のコンテキストでは、複数のanalyzeBodyInner呼び出しへと再帰するため、複数のreturn文があっても問題ありません。
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を介してターゲットに関する情報を取得し、ターゲットプラットフォームでfloatがサポートできるビット数を判断します。変換先のビット数が変換元より多ければ、型は安全に強制できます。そうでなければ、floatは切り詰められます。
Semaプロセスの完了
Semaは関数ごとに一度呼び出されます。これは、ファイル単位ではなく関数単位で実行される最初のプロセスです。すべての宣言を処理するのではなく、Semaは参照されている宣言ごとに一度だけ呼び出されます。参照されているすべての宣言を特定するために、Semaはプロセスのエントリポイント関数から開始します。
これはZigの「遅延解析」を実現しています。つまり、宣言が参照されない限り、特定のエラーはコンパイラエラーとして報告されません。これにより、参照されているコードのみがコンパイルされるため、コンパイルプロセスは極めて高速になり、生成されるコードも小さくなりますが、時として混乱を招く挙動にもつながります。
本ページで解説した基礎知識があれば、どんなZigプログラムでも、それがどのようにAIRへ翻訳されるかを追えるようになるはずです。Zigコンパイラが何を生成しているかを確認するために、zig ast-checkやzig build-obj --verbose-airコマンドを頻繁に使うことを忘れないでください。
この後、AIRは「codegen」プロセスに引き渡され、最終的な形式へとローワリングされます。Codegenは、共通のコンパイラ「フロントエンド」と複数の「バックエンド」との境界です。バックエンドはLLVMの場合もあれば、WASMのようなネイティブバックエンドの場合もあります。
記事をランダムに読む