feat(asm): Phase C.1 + D — inline asm codegen (runs end-to-end)

lowerAsmExpr stops bailing and builds the inline_asm op: resolves each operand's
effective name (§II.5 — explicit [name] else the {reg} pin), interns
template/constraints/clobbers, lowers input Refs, derives the result TypeId
(0→void, 1→T). Adds the last deferred validation (every %[name] must name an
operand). Multi-output (N>1) bails with a named "Phase E" diagnostic.

emitInlineAsm (backend/llvm/ops.zig) ports Zig's airAssembly: assembles the LLVM
constraint string (outputs → inputs → ~{clobber}, ',' → '|'), rewrites the
template (%[name]→${N}, %%→%, $→$$, %=→${:uid}), then LLVMGetInlineAsm +
LLVMBuildCall2 (AT&T dialect). Dispatch wired in emit_llvm.zig (replacing the C.0
@panic tripwire).

inferType gains an .asm_expr arm (expr_typer.zig) so a bare `x := asm {…-> T}`
binding types correctly — without it the binding inferred .unresolved and
silently produced 0.

llvm_shim.c: LLVMInitializeNativeAsmParser() — the JIT must assemble inline asm
at run time.

Verified end-to-end on the aarch64 host: `mov`/`add` with register-class inputs
and a value output run (exit 42/99), `nop volatile` runs (exit 0). IR is
textbook: `call i64 asm "add ${0},${1},${2}", "=r,r,r"(…)`.

Locked with 1645 (aarch64 add, runs; ir-only on non-aarch64) + 1646 (:= binding).
Updated 1640 (now Phase-E bail) + 1642 (now runs).

zig build test green (654 corpus, 446 unit).
This commit is contained in:
agra
2026-06-15 21:39:54 +03:00
parent 6c08de8ec1
commit 5a5e04c6d5
23 changed files with 395 additions and 50 deletions

View File

@@ -1563,11 +1563,7 @@ 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"),
.inline_asm => |a| self.ops().emitInlineAsm(instruction, a),
.call => |call_op| self.ops().emitCall(instruction, call_op),
.call_indirect => |call_op| self.ops().emitCallIndirect(instruction, call_op),

View File

@@ -398,6 +398,22 @@ pub const ExprTyper = struct {
}
break :blk self.l.inferExprType(nc.rhs);
},
// Inline asm result type from the `out_value` operands: 0 → void,
// 1 → that operand's type. N>1 (tuple) is Phase E → `.unresolved`
// here (lowering bails on it anyway). Mirrors `lowerAsmExpr`, so a
// bare `x := asm {…-> T}` binding types correctly.
.asm_expr => |ae| blk: {
var n_out: usize = 0;
var first_out: ?*Node = null;
for (ae.operands) |op| {
if (op.role != .out_value) continue;
n_out += 1;
if (first_out == null) first_out = op.payload;
}
if (n_out == 0) break :blk .void;
if (n_out == 1) break :blk self.l.resolveTypeWithBindings(first_out.?);
break :blk .unresolved;
},
// Statements don't produce values (`.return_stmt` is handled above
// as `.noreturn` — it diverges rather than yielding `void`).
.assignment, .var_decl, .const_decl, .fn_decl,

View File

@@ -2261,9 +2261,98 @@ pub fn lowerAsmExpr(self: *Lowering, ae: *const ast.AsmExpr, span: ast.Span) Ref
return self.emitPlaceholder("inline_asm");
}
// Shape is valid — codegen just isn't implemented yet (Phases CE).
diags.addFmt(.err, span, "inline assembly codegen is not yet implemented (ASM stream: lowering + emit land in Phases CE)", .{});
return self.emitPlaceholder("inline_asm");
// (4) Every `%[name]` in the template must name an operand (effective name:
// explicit `[name]` or auto-derived register). Caught here so emit's
// template rewriter never sees an unknown reference. §II.6.
{
const tmpl = ae.template.data.string_literal.raw;
var i: usize = 0;
while (i < tmpl.len) : (i += 1) {
if (tmpl[i] != '%' or i + 1 >= tmpl.len) continue;
const nxt = tmpl[i + 1];
if (nxt == '%' or nxt == '=') {
i += 1;
continue;
}
if (nxt != '[') continue;
const close = std.mem.indexOfScalarPos(u8, tmpl, i + 2, ']') orelse {
diags.addFmt(.err, span, "unterminated `%[` in asm template", .{});
return self.emitPlaceholder("inline_asm");
};
var ref_name = tmpl[i + 2 .. close];
if (std.mem.indexOfScalar(u8, ref_name, ':')) |colon| ref_name = ref_name[0..colon];
var found = false;
for (ae.operands) |op| {
const eff = op.name orelse (pinnedRegister(op.constraint) orelse "");
if (eff.len != 0 and std.mem.eql(u8, eff, ref_name)) {
found = true;
break;
}
}
if (!found) {
diags.addFmt(.err, span, "asm template references `%[{s}]` but no operand is named `{s}`", .{ ref_name, ref_name });
return self.emitPlaceholder("inline_asm");
}
i = close;
}
}
// ── Build the IR op (C.1). D emits 0 or 1 value output; N>1 (tuple result)
// is Phase E — bail loudly until then. ──
var n_value_outputs: usize = 0;
for (ae.operands) |op| {
if (op.role == .out_value) n_value_outputs += 1;
}
if (n_value_outputs > 1) {
diags.addFmt(.err, span, "multi-output (tuple-returning) inline assembly is not yet implemented (ASM stream Phase E)", .{});
return self.emitPlaceholder("inline_asm");
}
// Result type: 0 outputs → void; 1 → that operand's resolved type. (The
// resolver diagnoses an unresolvable type and returns `.unresolved`.)
var result_ty: TypeId = .void;
for (ae.operands) |op| {
if (op.role == .out_value) {
result_ty = self.resolveTypeWithBindings(op.payload);
break;
}
}
if (result_ty == .unresolved) return self.emitPlaceholder("inline_asm");
// IR operands, in source order (= `%N` index space + LLVM operand order).
const ir_ops = self.alloc.alloc(inst_mod.InlineAsm.AsmOperand, ae.operands.len) catch unreachable;
for (ae.operands, 0..) |op, i| {
// Effective name (design §II.5): explicit `[name]`, else auto-derived
// from a `{reg}` pin, else anonymous (`.empty`).
const eff_name: []const u8 = op.name orelse (pinnedRegister(op.constraint) orelse "");
ir_ops[i] = .{
.role = switch (op.role) {
.out_value => .out_value,
.out_place => .out_place,
.input => .input,
},
.name = if (eff_name.len == 0) types.StringId.empty else self.module.types.internString(eff_name),
.constraint = self.module.types.internString(op.constraint),
// input → the lowered value Ref; an output yields its value (none).
.operand = if (op.role == .input) self.lowerExpr(op.payload) else Ref.none,
};
}
const ir_clobbers = self.alloc.alloc(types.StringId, ae.clobbers.len) catch unreachable;
for (ae.clobbers, 0..) |cl, i| {
ir_clobbers[i] = self.module.types.internString(cl);
}
// Template text RAW — no sx escape processing (matches `#string` literal
// bytes; the `%[name]`/`%%`/`$` rewrite happens at emit). §II.11.
const template_text = ae.template.data.string_literal.raw;
return self.builder.emit(.{ .inline_asm = .{
.template = self.module.types.internString(template_text),
.operands = ir_ops,
.clobbers = ir_clobbers,
.has_side_effects = ae.is_volatile,
} }, result_ty);
}
/// If `node` names a `for xs: (*x)` by-ref capture (an `*elem`), returns