feat(asm): Phase C.0 — add inline_asm IR op (lock, no behavior change)
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).
This commit is contained in:
@@ -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).
|
and make it pass).
|
||||||
|
|
||||||
## Last completed step
|
## 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`,
|
`lowerAsmExpr` with a `pinnedRegister(constraint)` helper (`"={eax}"`→`eax`,
|
||||||
`"+{rax}"`→`rax`, `"=r"`→null) and two checks: (1) **reject the echo form**
|
`"+{rax}"`→`rax`, `"=r"`→null) and two checks: (1) **reject the echo form**
|
||||||
`[eax] "={eax}"` — a label identical to its own pinned register is redundant
|
`[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).
|
`extern`, 60 sites; `--target` a global CLI flag).
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
**C.0** (Phase C — IR op) — add `inline_asm: InlineAsm` to `Op` (`src/ir/inst.zig`,
|
**C.1 + D together** (must land as one green step) — wire `lowerAsmExpr` to BUILD
|
||||||
next to `objc_msg_send`) + the `AsmOperand` IR struct (role/name/constraint/operand
|
the `inline_asm` op (intern template + constraints + clobber names; resolve each
|
||||||
as `StringId`/`Ref`), and an interp `bailDetail` arm (`src/ir/interp.zig`) — inline
|
operand's effective name via the §II.5 auto-naming rule; lower input `Ref`s;
|
||||||
asm can never be comptime-evaluated. Unit-test the IR shape. This is a `lock`
|
compute the result `TypeId` from the `out_value` operands — 0→void, 1→T, N→tuple,
|
||||||
commit (no behavior change yet — `lowerAsmExpr` keeps bailing). Then C.1 wires
|
named) AND implement `emitInlineAsm` in `src/ir/emit_llvm.zig` (replacing the
|
||||||
`lowerAsmExpr` to actually intern strings + lower input `Ref`s + build the op and
|
`@panic` tripwire) — the port of Zig's `airAssembly`: assemble the LLVM constraint
|
||||||
compute the result `TypeId`, at which point **result-type derivation becomes
|
string (outputs `=`/`+`, inputs, `clobbers`→`~{name}`), rewrite `%[name]`→`${N}` /
|
||||||
observable** and the auto-naming/tuple typing (`expr_typer.zig`, deferred from B)
|
`%%` / `%=`, `LLVMGetInlineAsm` + `LLVMBuildCall2`, AT&T dialect. They land
|
||||||
can be tested end-to-end. See `PLAN-ASM.md` Phase C + design §II.6.
|
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
|
||||||
Remaining deferred validation (do alongside C, once template scanning is worth
|
`x86_64-linux` (ir-only via a `.build` `{ "target": "x86_64-linux" }` + `.ir`
|
||||||
it): `%[name]` references in the template that name no operand. Needs effective-
|
snapshot, since the host is aarch64). Result-type derivation for `expr_typer.zig`
|
||||||
name resolution (explicit `[name]` ∪ auto-derived register names).
|
(`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
|
## Log
|
||||||
- (init) Plan + design doc written; ASM stream opened.
|
- (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
|
- (B.1) operand-name validation: `pinnedRegister` helper + reject echo form
|
||||||
(`[eax] "={eax}"`) and duplicate names. Locked with 1643 + 1644. `zig build
|
(`[eax] "={eax}"`) and duplicate names. Locked with 1643 + 1644. `zig build
|
||||||
test` green (652 corpus, 445 unit).
|
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
|
## Known issues
|
||||||
- **0137** — `sx run` on a program with no `main` segfaults (unguarded JIT entry
|
- **0137** — `sx run` on a program with no `main` segfaults (unguarded JIT entry
|
||||||
|
|||||||
@@ -1563,6 +1563,11 @@ pub const LLVMEmitter = struct {
|
|||||||
// ── Calls ─────────────────────────────────────────────
|
// ── Calls ─────────────────────────────────────────────
|
||||||
.objc_msg_send => |msg| self.ops().emitObjcMsgSend(instruction, msg),
|
.objc_msg_send => |msg| self.ops().emitObjcMsgSend(instruction, msg),
|
||||||
.jni_msg_send => |msg| self.ops().emitJniMsgSend(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 => |call_op| self.ops().emitCall(instruction, call_op),
|
||||||
.call_indirect => |call_op| self.ops().emitCallIndirect(instruction, call_op),
|
.call_indirect => |call_op| self.ops().emitCallIndirect(instruction, call_op),
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const FuncId = inst_mod.FuncId;
|
|||||||
const Inst = inst_mod.Inst;
|
const Inst = inst_mod.Inst;
|
||||||
const Block = inst_mod.Block;
|
const Block = inst_mod.Block;
|
||||||
const Function = inst_mod.Function;
|
const Function = inst_mod.Function;
|
||||||
|
const InlineAsm = inst_mod.InlineAsm;
|
||||||
|
const StringId = types.StringId;
|
||||||
|
|
||||||
test "Ref none sentinel" {
|
test "Ref none sentinel" {
|
||||||
try std.testing.expect(Ref.none.isNone());
|
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);
|
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" {
|
test "function creation" {
|
||||||
const alloc = std.testing.allocator;
|
const alloc = std.testing.allocator;
|
||||||
const params = &[_]Function.Param{
|
const params = &[_]Function.Param{
|
||||||
|
|||||||
@@ -226,6 +226,13 @@ pub const Op = union(enum) {
|
|||||||
/// Method-ID caching across call sites is added in step 1.17.
|
/// Method-ID caching across call sites is added in step 1.17.
|
||||||
jni_msg_send: JniMsgSend,
|
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 creation ────────────────────────────────────────────
|
||||||
closure_create: ClosureCreate,
|
closure_create: ClosureCreate,
|
||||||
|
|
||||||
@@ -339,6 +346,35 @@ pub const ObjcMsgSend = struct {
|
|||||||
args: []const Ref, // additional args after recv + sel
|
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`
|
/// JNI dispatch payload. `env` is `JNIEnv*` (typed as ptr); `target`
|
||||||
/// is a `jobject` for instance calls and a `jclass` for static calls.
|
/// is a `jobject` for instance calls and a `jclass` for static calls.
|
||||||
/// `name` and `sig` are pointers to NUL-terminated bytes (typically
|
/// `name` and `sig` are pointers to NUL-terminated bytes (typically
|
||||||
|
|||||||
@@ -1015,6 +1015,8 @@ pub const Interpreter = struct {
|
|||||||
.objc_msg_send => return bailDetail("#objc_call not available at comptime (no Obj-C runtime)"),
|
.objc_msg_send => return bailDetail("#objc_call not available at comptime (no Obj-C runtime)"),
|
||||||
// Same story for JNI — no JVM at compile time.
|
// Same story for JNI — no JVM at compile time.
|
||||||
.jni_msg_send => return bailDetail("#jni_call not available at comptime (no JVM)"),
|
.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 params ────────────────────────────────────
|
||||||
.block_param => {
|
.block_param => {
|
||||||
|
|||||||
@@ -328,6 +328,14 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write
|
|||||||
try writeArgs(c.args, writer);
|
try writeArgs(c.args, writer);
|
||||||
try writer.writeAll(") : ");
|
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| {
|
.compiler_call => |cc| {
|
||||||
const name = tt.getString(@enumFromInt(cc.name));
|
const name = tt.getString(@enumFromInt(cc.name));
|
||||||
try writer.print("compiler_call \"{s}\"(", .{name});
|
try writer.print("compiler_call \"{s}\"(", .{name});
|
||||||
|
|||||||
Reference in New Issue
Block a user