feat(asm): Phase B.1 — operand-name validation (echo + duplicates)

Extends lowerAsmExpr with a pinnedRegister(constraint) helper and two §II.5
operand-naming checks, in the compile path before the codegen bail:

- reject the echo form `[eax] "={eax}"` — a label identical to the register its
  own constraint pins is redundant (the operand is already auto-named after the
  register); the useful form is a label that differs (`[quot] "={rax}"`);
- reject duplicate operand names (ambiguous %[name] / result field).

Locked with 1643-platform-asm-echo-name and 1644-platform-asm-duplicate-name.

zig build test green (652 corpus, 445 unit).
This commit is contained in:
agra
2026-06-15 20:41:41 +03:00
parent 1040b8c776
commit 5f444aae26
10 changed files with 86 additions and 15 deletions

View File

@@ -6,7 +6,17 @@ commit, one step at a time per the cadence rule (no commit may both add a test
and make it pass).
## Last completed step
**B.0** — asm shape validation (compile-path diagnostics). Restructured the
**B.1** — operand-name validation (design §II.5 auto-naming rule). Extended
`lowerAsmExpr` with a `pinnedRegister(constraint)` helper (`"={eax}"``eax`,
`"+{rax}"``rax`, `"=r"`→null) and two checks: (1) **reject the echo form**
`[eax] "={eax}"` — a label identical to its own pinned register is redundant
(the operand is already auto-named after the register); (2) **reject duplicate
operand names** (ambiguous `%[name]` / result field). Locked with
`examples/1643-platform-asm-echo-name.sx` + `1644-platform-asm-duplicate-name.sx`.
`zig build test` green (652 corpus, 0 failed; 445 unit). Files:
`src/ir/lower/expr.zig`.
Prior: **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
@@ -83,20 +93,19 @@ Phase BE feasibility already confirmed against the live tree
`extern`, 60 sites; `--target` a global CLI flag).
## Next step
**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.
**C.0** (Phase C — IR op) — add `inline_asm: InlineAsm` to `Op` (`src/ir/inst.zig`,
next to `objc_msg_send`) + the `AsmOperand` IR struct (role/name/constraint/operand
as `StringId`/`Ref`), and an interp `bailDetail` arm (`src/ir/interp.zig`) — inline
asm can never be comptime-evaluated. Unit-test the IR shape. This is a `lock`
commit (no behavior change yet — `lowerAsmExpr` keeps bailing). Then C.1 wires
`lowerAsmExpr` to actually intern strings + lower input `Ref`s + build the op and
compute the result `TypeId`, at which point **result-type derivation becomes
observable** and the auto-naming/tuple typing (`expr_typer.zig`, deferred from B)
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.)
Remaining deferred validation (do alongside C, once template scanning is worth
it): `%[name]` references in the template that name no operand. Needs effective-
name resolution (explicit `[name]` auto-derived register names).
## Log
- (init) Plan + design doc written; ASM stream opened.
@@ -120,6 +129,9 @@ makes it observable — see Current state.)
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).
- (B.1) operand-name validation: `pinnedRegister` helper + reject echo form
(`[eax] "={eax}"`) and duplicate names. Locked with 1643 + 1644. `zig build
test` green (652 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 — operand naming rule (§II.5): an explicit `[name]` that
// just echoes the register its own constraint pins (`[eax] "={eax}"`) carries
// no information — the operand is already auto-named after the register. Reject
// it. The useful form is a label that DIFFERS (e.g. `[quot] "={rax}"`).
f :: () -> u32 { return asm volatile { "cpuid", [eax] "={eax}" -> u32, "{eax}" = 1 }; }
main :: () { x := f(); }

View File

@@ -0,0 +1,4 @@
// ASM stream Phase B — two asm operands may not share a `[name]`: the `%[name]`
// template reference (and the result tuple field) would be ambiguous.
f :: () -> u64 { return asm volatile { "nop", [x] "=r" -> u64, [x] "r" = 5 }; }
main :: () { v := f(); }

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: redundant asm operand name `eax` — it already names the pinned register; drop the `[eax]`
--> examples/1643-platform-asm-echo-name.sx:5:25
|
5 | f :: () -> u32 { return asm volatile { "cpuid", [eax] "={eax}" -> u32, "{eax}" = 1 }; }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: duplicate asm operand name `x`
--> examples/1644-platform-asm-duplicate-name.sx:3:25
|
3 | f :: () -> u64 { return asm volatile { "nop", [x] "=r" -> u64, [x] "r" = 5 }; }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -2194,6 +2194,17 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
};
}
/// The single register a constraint pins, or null for a register-class /
/// memory constraint. Strips a leading `=`/`+` (output / read-write marker),
/// then returns the `{reg}` body. `"={eax}"` → `eax`, `"+{rax}"` → `rax`,
/// `"{rdi}"` → `rdi`; `"=r"` / `"r"` / `"=m"` → null.
fn pinnedRegister(constraint: []const u8) ?[]const u8 {
var c = constraint;
if (c.len > 0 and (c[0] == '=' or c[0] == '+')) c = c[1..];
if (c.len >= 2 and c[0] == '{' and c[c.len - 1] == '}') return c[1 .. c.len - 1];
return null;
}
/// 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 +
@@ -2215,7 +2226,31 @@ pub fn lowerAsmExpr(self: *Lowering, ae: *const ast.AsmExpr, span: ast.Span) Ref
return self.emitPlaceholder("inline_asm");
}
// (2) An asm with no value outputs yields no result, so it must be
// (2) Operand-name validation (design §II.5 auto-naming rule). For each
// explicit `[name]`:
// - reject the ECHO form `[eax] "={eax}"` — a label identical to the
// register its own constraint pins carries no information (the operand
// is already auto-named after that register); and
// - reject DUPLICATE names — `%[name]` / the result field would be
// ambiguous.
for (ae.operands, 0..) |op, i| {
const name = op.name orelse continue;
if (pinnedRegister(op.constraint)) |reg| {
if (std.mem.eql(u8, name, reg)) {
diags.addFmt(.err, span, "redundant asm operand name `{s}` — it already names the pinned register; drop the `[{s}]`", .{ name, name });
return self.emitPlaceholder("inline_asm");
}
}
for (ae.operands[0..i]) |prev| {
const pname = prev.name orelse continue;
if (std.mem.eql(u8, name, pname)) {
diags.addFmt(.err, span, "duplicate asm operand name `{s}`", .{name});
return self.emitPlaceholder("inline_asm");
}
}
}
// (3) 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| {