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:
@@ -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 B–E 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
|
||||
|
||||
6
examples/1641-platform-asm-missing-volatile.sx
Normal file
6
examples/1641-platform-asm-missing-volatile.sx
Normal 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(); }
|
||||
5
examples/1642-platform-asm-nop-volatile.sx
Normal file
5
examples/1642-platform-asm-nop-volatile.sx
Normal 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 C–E). Confirms the volatile rule's positive side.
|
||||
nop :: () { asm volatile { "nop" }; }
|
||||
main :: () { nop(); }
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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" }; }
|
||||
| ^^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/expected/1642-platform-asm-nop-volatile.exit
Normal file
1
examples/expected/1642-platform-asm-nop-volatile.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
5
examples/expected/1642-platform-asm-nop-volatile.stderr
Normal file
5
examples/expected/1642-platform-asm-nop-volatile.stderr
Normal file
@@ -0,0 +1,5 @@
|
||||
error: inline assembly codegen is not yet implemented (ASM stream: lowering + emit land in Phases C–E)
|
||||
--> examples/1642-platform-asm-nop-volatile.sx:4:13
|
||||
|
|
||||
4 | nop :: () { asm volatile { "nop" }; }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
1
examples/expected/1642-platform-asm-nop-volatile.stdout
Normal file
1
examples/expected/1642-platform-asm-nop-volatile.stdout
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 C–E). 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 C–E)", .{});
|
||||
}
|
||||
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 C–E; 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 C–E).
|
||||
diags.addFmt(.err, span, "inline assembly codegen is not yet implemented (ASM stream: lowering + emit land in Phases C–E)", .{});
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user