From 6c08de8ec1e0db2af681d66b5b8249035cc5e33b Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 15 Jun 2026 21:00:12 +0300 Subject: [PATCH] =?UTF-8?q?feat(asm):=20Phase=20C.0=20=E2=80=94=20add=20in?= =?UTF-8?q?line=5Fasm=20IR=20op=20(lock,=20no=20behavior=20change)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the `inline_asm: InlineAsm` opcode to the IR Op union (inst.zig): interned template + operand list (role/name/constraint/operand) + interned clobber names + has_side_effects; the result rides on Inst.ty (void / scalar / tuple). The new variant forces coverage in the exhaustive Op switches: - interp.zig: loud bailDetail — inline asm is never comptime-evaluable. - print.zig: an IR-dump arm. - emit_llvm.zig: a @panic TRIPWIRE — emit lands in Phase D, and until then lowerAsmExpr still bails, so no inline_asm op is ever created. Reaching emit would mean lowering switched over before emit was ready; crash loudly rather than miscompile. No behavior change: lowering still bails, the op is constructed only in the new `inline_asm op shape` unit test (inst.test.zig). zig build test green (652 corpus, 446 unit). --- current/CHECKPOINT-ASM.md | 47 +++++++++++++++++++++++++++------------ src/ir/emit_llvm.zig | 5 +++++ src/ir/inst.test.zig | 36 ++++++++++++++++++++++++++++++ src/ir/inst.zig | 36 ++++++++++++++++++++++++++++++ src/ir/interp.zig | 2 ++ src/ir/print.zig | 8 +++++++ 6 files changed, 120 insertions(+), 14 deletions(-) diff --git a/current/CHECKPOINT-ASM.md b/current/CHECKPOINT-ASM.md index a2bee9f..7ee5d5c 100644 --- a/current/CHECKPOINT-ASM.md +++ b/current/CHECKPOINT-ASM.md @@ -6,7 +6,21 @@ commit, one step at a time per the cadence rule (no commit may both add a test and make it pass). ## Last completed step -**B.1** — operand-name validation (design §II.5 auto-naming rule). Extended +**C.0** — IR op `inline_asm` (lock; no behavior change). Added `inline_asm: +InlineAsm` to the IR `Op` union + the `InlineAsm` struct (`template: StringId`, +`operands: []const AsmOperand` {role/name/constraint/operand}, `clobbers: +[]const StringId`, `has_side_effects`) in `src/ir/inst.zig` — all strings +interned, operands in source order, result on `Inst.ty`. The new variant forced +(and got) arms in two exhaustive `Op` switches: `src/ir/interp.zig` (loud +`bailDetail` — inline asm is never comptime-evaluable) and `src/ir/print.zig` +(IR dump). `src/ir/emit_llvm.zig` gets a `@panic` **tripwire** — emit lands in +Phase D, and until then `lowerAsmExpr` still bails so no `inline_asm` op is ever +created (reaching emit would be a lowering-switched-over-too-early bug). Unit +test `inline_asm op shape` in `src/ir/inst.test.zig`. `zig build test` green +(652 corpus, 446 unit). Files: `src/ir/inst.zig`, `src/ir/interp.zig`, +`src/ir/print.zig`, `src/ir/emit_llvm.zig`, `src/ir/inst.test.zig`. + +Prior: **B.1** — operand-name validation (design §II.5 auto-naming rule). Extended `lowerAsmExpr` with a `pinnedRegister(constraint)` helper (`"={eax}"`→`eax`, `"+{rax}"`→`rax`, `"=r"`→null) and two checks: (1) **reject the echo form** `[eax] "={eax}"` — a label identical to its own pinned register is redundant @@ -93,19 +107,21 @@ Phase B–E feasibility already confirmed against the live tree `extern`, 60 sites; `--target` a global CLI flag). ## Next step -**C.0** (Phase C — IR op) — add `inline_asm: InlineAsm` to `Op` (`src/ir/inst.zig`, -next to `objc_msg_send`) + the `AsmOperand` IR struct (role/name/constraint/operand -as `StringId`/`Ref`), and an interp `bailDetail` arm (`src/ir/interp.zig`) — inline -asm can never be comptime-evaluated. Unit-test the IR shape. This is a `lock` -commit (no behavior change yet — `lowerAsmExpr` keeps bailing). Then C.1 wires -`lowerAsmExpr` to actually intern strings + lower input `Ref`s + build the op and -compute the result `TypeId`, at which point **result-type derivation becomes -observable** and the auto-naming/tuple typing (`expr_typer.zig`, deferred from B) -can be tested end-to-end. See `PLAN-ASM.md` Phase C + design §II.6. - -Remaining deferred validation (do alongside C, once template scanning is worth -it): `%[name]` references in the template that name no operand. Needs effective- -name resolution (explicit `[name]` ∪ auto-derived register names). +**C.1 + D together** (must land as one green step) — wire `lowerAsmExpr` to BUILD +the `inline_asm` op (intern template + constraints + clobber names; resolve each +operand's effective name via the §II.5 auto-naming rule; lower input `Ref`s; +compute the result `TypeId` from the `out_value` operands — 0→void, 1→T, N→tuple, +named) AND implement `emitInlineAsm` in `src/ir/emit_llvm.zig` (replacing the +`@panic` tripwire) — the port of Zig's `airAssembly`: assemble the LLVM constraint +string (outputs `=`/`+`, inputs, `clobbers`→`~{name}`), rewrite `%[name]`→`${N}` / +`%%` / `%=`, `LLVMGetInlineAsm` + `LLVMBuildCall2`, AT&T dialect. They land +together because the moment lowering stops bailing, emit is reached — a half-step +would hit the tripwire. First target: the single-value-output syscall on +`x86_64-linux` (ir-only via a `.build` `{ "target": "x86_64-linux" }` + `.ir` +snapshot, since the host is aarch64). Result-type derivation for `expr_typer.zig` +(`inferType` `.asm_expr` arm) also lands here — now observable. Then E (multi- +return tuples) + remaining validation (`%[name]` references a real operand). See +`PLAN-ASM.md` Phases C–E + design §II.6. ## Log - (init) Plan + design doc written; ASM stream opened. @@ -132,6 +148,9 @@ name resolution (explicit `[name]` ∪ auto-derived register names). - (B.1) operand-name validation: `pinnedRegister` helper + reject echo form (`[eax] "={eax}"`) and duplicate names. Locked with 1643 + 1644. `zig build test` green (652 corpus, 445 unit). +- (C.0) IR op `inline_asm: InlineAsm` + interp `bailDetail` + print arm + emit + `@panic` tripwire (Phase D). No behavior change (lowering still bails). Unit + test `inline_asm op shape`. `zig build test` green (652 corpus, 446 unit). ## Known issues - **0137** — `sx run` on a program with no `main` segfaults (unguarded JIT entry diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index a49373a..ea4430f 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -1563,6 +1563,11 @@ pub const LLVMEmitter = struct { // ── Calls ───────────────────────────────────────────── .objc_msg_send => |msg| self.ops().emitObjcMsgSend(instruction, msg), .jni_msg_send => |msg| self.ops().emitJniMsgSend(instruction, msg), + // Tripwire (ASM stream): the IR op exists (Phase C.0) but emit lands + // in Phase D. Until then `lowerAsmExpr` still bails, so no inline_asm + // op is ever created — reaching here means lowering switched over + // before emit was ready. Crash loudly rather than miscompile. + .inline_asm => @panic("inline_asm reached LLVM emit before Phase D — lowering must still bail until emitInlineAsm lands"), .call => |call_op| self.ops().emitCall(instruction, call_op), .call_indirect => |call_op| self.ops().emitCallIndirect(instruction, call_op), diff --git a/src/ir/inst.test.zig b/src/ir/inst.test.zig index b25ce82..da0f0f1 100644 --- a/src/ir/inst.test.zig +++ b/src/ir/inst.test.zig @@ -10,6 +10,8 @@ const FuncId = inst_mod.FuncId; const Inst = inst_mod.Inst; const Block = inst_mod.Block; const Function = inst_mod.Function; +const InlineAsm = inst_mod.InlineAsm; +const StringId = types.StringId; test "Ref none sentinel" { try std.testing.expect(Ref.none.isNone()); @@ -48,6 +50,40 @@ test "block creation" { try std.testing.expectEqual(@as(usize, 2), block.insts.items.len); } +test "inline_asm op shape (ASM stream Phase C.0)" { + // out_value (yields the value, operand = .none) + a named-less input, + // plus two clobbers; result rides on Inst.ty. + const operands = [_]InlineAsm.AsmOperand{ + .{ .role = .out_value, .name = @enumFromInt(1), .constraint = @enumFromInt(2), .operand = Ref.none }, + .{ .role = .input, .name = .empty, .constraint = @enumFromInt(3), .operand = Ref.fromIndex(5) }, + }; + const clobbers = [_]StringId{ @enumFromInt(4), @enumFromInt(6) }; + const inst = Inst{ + .op = .{ .inline_asm = .{ + .template = @enumFromInt(10), + .operands = &operands, + .clobbers = &clobbers, + .has_side_effects = true, + } }, + .ty = .i64, + }; + switch (inst.op) { + .inline_asm => |a| { + try std.testing.expect(a.has_side_effects); + try std.testing.expectEqual(@as(usize, 2), a.operands.len); + try std.testing.expectEqual(@as(usize, 2), a.clobbers.len); + try std.testing.expectEqual(InlineAsm.AsmOperand.Role.out_value, a.operands[0].role); + // an out_value operand carries no input Ref — the asm yields it + try std.testing.expect(a.operands[0].operand.isNone()); + try std.testing.expectEqual(InlineAsm.AsmOperand.Role.input, a.operands[1].role); + try std.testing.expectEqual(Ref.fromIndex(5), a.operands[1].operand); + // an anonymous operand uses the `.empty` StringId sentinel + try std.testing.expectEqual(StringId.empty, a.operands[1].name); + }, + else => unreachable, + } +} + test "function creation" { const alloc = std.testing.allocator; const params = &[_]Function.Param{ diff --git a/src/ir/inst.zig b/src/ir/inst.zig index ba335ce..28c8ab6 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -226,6 +226,13 @@ pub const Op = union(enum) { /// Method-ID caching across call sites is added in step 1.17. jni_msg_send: JniMsgSend, + /// `asm volatile? { "tmpl", operands…, clobbers(.…) }` — inline assembly + /// (ASM stream, design §II.6). emit_llvm.zig assembles the LLVM constraint + /// string + rewrites the `%[name]` template, then `LLVMGetInlineAsm` + + /// `LLVMBuildCall2`. The result rides on `Inst.ty` (void / a scalar / a tuple + /// of the `out_value` types). Never comptime-evaluable — the interp bails. + inline_asm: InlineAsm, + // ── Closure creation ──────────────────────────────────────────── closure_create: ClosureCreate, @@ -339,6 +346,35 @@ pub const ObjcMsgSend = struct { args: []const Ref, // additional args after recv + sel }; +/// Inline assembly payload (design §II.6). All strings interned; operands in +/// SOURCE ORDER (= the `%N` index space and the LLVM constraint order). The +/// result type rides on `Inst.ty`: void (no value outputs), a scalar (one), or +/// a tuple (N). emit_llvm.zig owns the constraint-string assembly + `%[name]` +/// template rewrite. +pub const InlineAsm = struct { + /// Interned template, RAW — the `%[name]`→`${N}` rewrite happens at emit. + template: StringId, + /// Declaration order preserved (keys `%N` and the LLVM operand order). + operands: []const AsmOperand, + /// Interned dot-names from `clobbers(.…)`: "rcx", "cc", "memory", … + clobbers: []const StringId, + /// `volatile` — passed as LLVM `HasSideEffects`. + has_side_effects: bool, + + pub const AsmOperand = struct { + role: Role, + /// Effective operand name (explicit `[name]` or auto-derived register); + /// `.empty` when anonymous. + name: StringId, + /// Verbatim constraint, e.g. "={rax}", "=r", "+r", "{rdi}", "r". + constraint: StringId, + /// `input` → the value `Ref`; `out_value` → `.none` (the asm yields it). + operand: Ref, + + pub const Role = enum { out_value, out_place, input }; + }; +}; + /// JNI dispatch payload. `env` is `JNIEnv*` (typed as ptr); `target` /// is a `jobject` for instance calls and a `jclass` for static calls. /// `name` and `sig` are pointers to NUL-terminated bytes (typically diff --git a/src/ir/interp.zig b/src/ir/interp.zig index a75ecba..0d18c3a 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -1015,6 +1015,8 @@ pub const Interpreter = struct { .objc_msg_send => return bailDetail("#objc_call not available at comptime (no Obj-C runtime)"), // Same story for JNI — no JVM at compile time. .jni_msg_send => return bailDetail("#jni_call not available at comptime (no JVM)"), + // Inline asm executes target machine code — never comptime-evaluable. + .inline_asm => return bailDetail("inline assembly requires native execution; not available at comptime"), // ── Block params ──────────────────────────────────── .block_param => { diff --git a/src/ir/print.zig b/src/ir/print.zig index d1c850b..483d4db 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -328,6 +328,14 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write try writeArgs(c.args, writer); try writer.writeAll(") : "); }, + .inline_asm => |a| { + try writer.print("inline_asm{s} tmpl=#{d} ops={d} clobbers={d} : ", .{ + if (a.has_side_effects) " volatile" else "", + a.template.index(), + a.operands.len, + a.clobbers.len, + }); + }, .compiler_call => |cc| { const name = tt.getString(@enumFromInt(cc.name)); try writer.print("compiler_call \"{s}\"(", .{name});