feat(asm): Phase B.0 — validate asm shape in the compile path

Restructures the .asm_expr lowering arm into lowerAsmExpr, which validates the
asm shape with specific named diagnostics BEFORE the not-yet-implemented codegen
bail, so the user sees the real problem first. Two checklist items enforced:

- template must be a compile-time-known string ("..." or #string), not a
  runtime expression;
- an asm with no value outputs must be `volatile` (else its effects could be
  deleted) — mirrors Zig's rule.

Valid shapes still bail with the "codegen not yet implemented" message. Result-
type derivation + the operand auto-naming rule stay deferred to Phase C, where a
real IR op makes the result type observable/testable.

Locked with 1641-platform-asm-missing-volatile (the volatile error) and
1642-platform-asm-nop-volatile (no-output + volatile accepted → codegen bail).

zig build test green (650 corpus, 445 unit).
This commit is contained in:
agra
2026-06-15 20:35:43 +03:00
parent f8e029d719
commit 1040b8c776
11 changed files with 97 additions and 19 deletions

View File

@@ -1933,6 +1933,7 @@ pub const Lowering = struct {
pub const lowerNullCoalesce = lower_expr.lowerNullCoalesce;
pub const resolveOptionalInner = lower_expr.resolveOptionalInner;
pub const lowerExpr = lower_expr.lowerExpr;
pub const lowerAsmExpr = lower_expr.lowerAsmExpr;
pub const refCapturePointee = lower_expr.refCapturePointee;
pub const lowerBinaryOp = lower_expr.lowerBinaryOp;
pub const lowerTupleOp = lower_expr.lowerTupleOp;

View File

@@ -2189,20 +2189,48 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
.try_expr => |te| self.lowerTry(te.operand, node.span),
.catch_expr => |ce| self.lowerCatch(&ce, node.span),
.caller_location => self.lowerCallerLocation(node),
// Inline assembly parses (Phase A.1) but has no IR op / emit yet
// (Phases CE). Bail LOUDLY with a named diagnostic rather than falling
// into the generic `unknown_expr` arm — the placeholder Ref makes
// `hasErrors()` abort the build on this message (CLAUDE.md no-silent-arm).
.asm_expr => blk: {
if (self.diagnostics) |diags| {
diags.addFmt(.err, node.span, "inline assembly codegen is not yet implemented (ASM stream: lowering + emit land in Phases CE)", .{});
}
break :blk self.emitPlaceholder("inline_asm");
},
.asm_expr => |ae| self.lowerAsmExpr(&ae, node.span),
else => self.emitError("unknown_expr", node.span),
};
}
/// Inline assembly lowering. Phase B (partial): validate the asm shape in the
/// compile path with specific named diagnostics, THEN bail on the not-yet-
/// implemented codegen so the user sees the real problem first (the IR op +
/// LLVM emit land in Phases CE; result-type derivation + the auto-naming rule
/// move to the expression typer once lowering produces a real value). Always
/// returns a placeholder Ref so `hasErrors()` aborts the build on whichever
/// diagnostic fired (CLAUDE.md no-silent-arm).
pub fn lowerAsmExpr(self: *Lowering, ae: *const ast.AsmExpr, span: ast.Span) Ref {
const diags = self.diagnostics orelse return self.emitPlaceholder("inline_asm");
// (1) The template must be a compile-time-known string (a `"..."` literal or
// a `#string` heredoc), not a runtime expression.
const template_is_string = switch (ae.template.data) {
.string_literal => true,
else => false,
};
if (!template_is_string) {
diags.addFmt(.err, ae.template.span, "asm template must be a compile-time-known string", .{});
return self.emitPlaceholder("inline_asm");
}
// (2) An asm with no value outputs yields no result, so it must be
// `volatile` — otherwise its effects could be deleted. Mirrors Zig's rule.
var n_outputs: usize = 0;
for (ae.operands) |op| {
if (op.role == .out_value) n_outputs += 1;
}
if (n_outputs == 0 and !ae.is_volatile) {
diags.addFmt(.err, span, "asm expression with no outputs must be marked `volatile`", .{});
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");
}
/// If `node` names a `for xs: (*x)` by-ref capture (an `*elem`), returns
/// the element (pointee) type so a value-position use can auto-deref it.
pub fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId {