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:
@@ -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 B–E 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
|
||||
|
||||
6
examples/1643-platform-asm-echo-name.sx
Normal file
6
examples/1643-platform-asm-echo-name.sx
Normal 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(); }
|
||||
4
examples/1644-platform-asm-duplicate-name.sx
Normal file
4
examples/1644-platform-asm-duplicate-name.sx
Normal 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(); }
|
||||
1
examples/expected/1643-platform-asm-echo-name.exit
Normal file
1
examples/expected/1643-platform-asm-echo-name.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
5
examples/expected/1643-platform-asm-echo-name.stderr
Normal file
5
examples/expected/1643-platform-asm-echo-name.stderr
Normal 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 }; }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
1
examples/expected/1643-platform-asm-echo-name.stdout
Normal file
1
examples/expected/1643-platform-asm-echo-name.stdout
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/expected/1644-platform-asm-duplicate-name.exit
Normal file
1
examples/expected/1644-platform-asm-duplicate-name.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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 }; }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user