feat(asm): Phase 2 — -> @place write-through outputs

An asm result can be STORED through a place (a local / struct field) instead of
returned; the place output does not join the result tuple.

- parser.zig: `-> @place` parses `@place` as an ordinary address-of expression
  → an out_place operand (the in-function form; reuses the existing `@` prefix).
- inst.zig: AsmOperand gains out_ty (the output slot's value type) so emit can
  build the combined return struct without re-deriving from Inst.ty.
- lower/expr.zig: out_place operand = the lowered @place address, out_ty = the
  pointee. Read-write (`+`) and indirect-memory (`*`) constraints rejected loudly
  (not yet implemented) rather than miscompiled.
- ops.zig emitInlineAsm: the LLVM return type is built from ALL outputs
  (out_value + out_place); after the call, out_place slots are stored through
  their address and out_value slots rebuild the sx result. Fast path when there
  are no place outputs (the struct return IS the result — pure-value asm IR
  unchanged).

Verified: write-to-local (42), struct field, mixed value+place (v=10 b=20), `+`
rejected. Locked with 1649-platform-asm-place-output (mixed, runs on aarch64).

zig build test green (657 corpus, 446 unit).
This commit is contained in:
agra
2026-06-15 22:47:34 +03:00
parent b8800a234c
commit 967005621a
11 changed files with 198 additions and 24 deletions

View File

@@ -368,8 +368,15 @@ pub const InlineAsm = struct {
name: StringId,
/// Verbatim constraint, e.g. "={rax}", "=r", "+r", "{rdi}", "r".
constraint: StringId,
/// `input` → the value `Ref`; `out_value` → `.none` (the asm yields it).
/// `input` → the value `Ref`; `out_value` → `.none` (the asm yields it);
/// `out_place` → the place ADDRESS `Ref` (a pointer; the asm result is
/// `store`d through it).
operand: Ref,
/// The value type carried by an OUTPUT slot — `out_value`: its result
/// type; `out_place`: the pointee type stored through `operand`. `.void`
/// for inputs (their type comes from the input `Ref`). Lets emit build
/// the combined LLVM return struct without re-deriving from `Inst.ty`.
out_ty: TypeId = .void,
pub const Role = enum { out_value, out_place, input };
};

View File

@@ -2339,6 +2339,36 @@ pub fn lowerAsmExpr(self: *Lowering, ae: *const ast.AsmExpr, span: ast.Span) Ref
// 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 "");
var operand_ref: Ref = Ref.none;
var out_ty: TypeId = .void;
switch (op.role) {
.input => operand_ref = self.lowerExpr(op.payload),
.out_value => out_ty = self.resolveTypeWithBindings(op.payload),
.out_place => {
// Read-write (`+`) and indirect-memory (`*`) place outputs aren't
// implemented yet — reject loudly rather than miscompile (§II.11).
if (op.constraint.len > 0 and op.constraint[0] == '+') {
diags.addFmt(.err, span, "read-write (`+`) asm outputs are not yet implemented; use a write-only `=` output", .{});
return self.emitPlaceholder("inline_asm");
}
if (std.mem.indexOfScalar(u8, op.constraint, '*') != null) {
diags.addFmt(.err, span, "indirect-memory (`*`) asm outputs are not yet implemented", .{});
return self.emitPlaceholder("inline_asm");
}
// `@place` lowers to its address (a pointer); the asm result is
// stored through it. The stored type is the pointee.
operand_ref = self.lowerExpr(op.payload);
const pty = self.inferExprType(op.payload);
out_ty = if (!pty.isBuiltin()) blk: {
const info = self.module.types.get(pty);
break :blk if (info == .pointer) info.pointer.pointee else .unresolved;
} else .unresolved;
if (out_ty == .unresolved) {
diags.addFmt(.err, span, "asm `-> @place` output target must be an addressable place", .{});
return self.emitPlaceholder("inline_asm");
}
},
}
ir_ops[i] = .{
.role = switch (op.role) {
.out_value => .out_value,
@@ -2347,8 +2377,8 @@ pub fn lowerAsmExpr(self: *Lowering, ae: *const ast.AsmExpr, span: ast.Span) Ref
},
.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,
.operand = operand_ref,
.out_ty = out_ty,
};
}