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

@@ -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
**A.1** — parse `asm { … }` + loud lowering bail (folded A.1+A.2 into one honest
**B.0** — asm shape validation (compile-path diagnostics). Restructured the
`.asm_expr` lowering arm into `lowerAsmExpr` (`src/ir/lower/expr.zig`, mixed into
`Lowering` in `src/ir/lower.zig`): it validates BEFORE the not-yet-implemented
codegen bail, so the user sees the real problem first. Two checklist items now
enforced with named diagnostics: (1) **template must be a compile-time-known
string** (`"..."` / `#string`); (2) **no value outputs ⇒ must be `volatile`**
(mirrors Zig — a result-less asm could be deleted). Valid shapes still bail with
the "codegen not yet implemented" message. Result-type derivation + auto-naming
stay deferred to a later step (observable only once Phase C produces a real IR
op). Locked with `examples/1641-platform-asm-missing-volatile.sx` (volatile
error) + `1642-platform-asm-nop-volatile.sx` (volatile no-output accepted →
codegen bail). `zig build test` green (650 corpus, 0 failed; 445 unit). Files:
`src/ir/lower/expr.zig`, `src/ir/lower.zig`, `examples/164{1,2}-*`.
Prior: **A.1** — parse `asm { … }` + loud lowering bail (folded A.1+A.2 into one honest
lock commit, since the loud bail IS current correct behavior — cadence option
(a)). Added `AsmExpr`/`AsmOperand` to `src/ast.zig` + the `asm_expr` `Node.Data`
arm; `parseAsmExpr` in `src/parser.zig` (`parsePrimary` `.kw_asm` dispatch) —
@@ -69,14 +83,20 @@ Phase BE feasibility already confirmed against the live tree
`extern`, 60 sites; `--target` a global CLI flag).
## Next step
**B.0/B.1** (Phase B — sema/typing) — derive the asm result type from the
`out_value` operands (0→`void` + require `volatile`; 1→`T`; N→tuple, named via the
§II.5 auto-naming rule), in the expression typer (`src/ir/expr_typer.zig` /
`inferExprType`). Implement the validation checklist (no-output⇒volatile; layout;
comptime-string template; coerce comptime int→i64/float→f64) + the auto-naming /
echo-rejection diagnostics. On failure return the `.unresolved` sentinel, never a
silent default. Pin error-message examples. See `PLAN-ASM.md` Phase B + design
§II.5. (Lowering keeps bailing until Phase C adds the IR op.)
**B.1 / C.0** — two viable directions:
- **B.1 (more validation, testable now):** remaining checklist items that fire
pre-codegen — echo-name rejection (`[eax] "={eax}"`, §II.5 auto-naming),
duplicate operand names, `%[name]` references that name no operand. Add to
`lowerAsmExpr` (same compile-path pattern). Each is a pinnable error example.
- **C.0 (IR op, unlocks the rest):** add `inline_asm: InlineAsm` to `Op`
(`src/ir/inst.zig`) + interp `bailDetail`; then `lowerAsmExpr` stops bailing
and builds the op, at which point **result-type derivation becomes observable**
(so the auto-naming rule + tuple result typing in `expr_typer.zig` can be
tested end-to-end). See `PLAN-ASM.md` Phase C + design §II.6.
Recommended: finish the cheap B.1 validation diagnostics, then move to C.0.
(Result-type derivation in `expr_typer.zig` is intentionally deferred until C
makes it observable — see Current state.)
## Log
- (init) Plan + design doc written; ASM stream opened.
@@ -96,6 +116,10 @@ silent default. Pin error-message examples. See `PLAN-ASM.md` Phase B + design
exhaustive `Node.Data` switches; `-> @place` rejected (Phase 2). Adopted operand
auto-naming rule (design §II.5). Locked with 1640 fixture. Filed orthogonal
issue 0137 (no-`main` JIT segfault). `zig build test` green (648 corpus, 445 unit).
- (B.0) asm shape validation in `lowerAsmExpr`: comptime-string template +
no-output⇒volatile, with named diagnostics before the codegen bail. Locked with
1641 (volatile error) + 1642 (volatile accepted). `zig build test` green (650
corpus, 445 unit).
## Known issues
- **0137** — `sx run` on a program with no `main` segfaults (unguarded JIT entry

View File

@@ -0,0 +1,6 @@
// ASM stream Phase B — an asm with no value outputs yields no result, so its
// effects could be deleted unless it is marked `volatile`. This omits
// `volatile` ⇒ a compile error. Pins that diagnostic (mirrors Zig's rule).
// Called from `main` so lowering reaches the asm body.
nope :: () { asm { "nop" }; }
main :: () { nope(); }

View File

@@ -0,0 +1,5 @@
// ASM stream Phase B — the no-output form IS accepted when `volatile` is
// present: validation passes, and lowering then bails on the not-yet-
// implemented codegen (Phases CE). Confirms the volatile rule's positive side.
nop :: () { asm volatile { "nop" }; }
main :: () { nop(); }

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: asm expression with no outputs must be marked `volatile`
--> examples/1641-platform-asm-missing-volatile.sx:5:14
|
5 | nope :: () { asm { "nop" }; }
| ^^^^^^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: inline assembly codegen is not yet implemented (ASM stream: lowering + emit land in Phases CE)
--> examples/1642-platform-asm-nop-volatile.sx:4:13
|
4 | nop :: () { asm volatile { "nop" }; }
| ^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1 @@

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 {