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).
|
and make it pass).
|
||||||
|
|
||||||
## Last completed step
|
## 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
|
`.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
|
`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
|
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).
|
`extern`, 60 sites; `--target` a global CLI flag).
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
**B.1 / C.0** — two viable directions:
|
**C.0** (Phase C — IR op) — add `inline_asm: InlineAsm` to `Op` (`src/ir/inst.zig`,
|
||||||
- **B.1 (more validation, testable now):** remaining checklist items that fire
|
next to `objc_msg_send`) + the `AsmOperand` IR struct (role/name/constraint/operand
|
||||||
pre-codegen — echo-name rejection (`[eax] "={eax}"`, §II.5 auto-naming),
|
as `StringId`/`Ref`), and an interp `bailDetail` arm (`src/ir/interp.zig`) — inline
|
||||||
duplicate operand names, `%[name]` references that name no operand. Add to
|
asm can never be comptime-evaluated. Unit-test the IR shape. This is a `lock`
|
||||||
`lowerAsmExpr` (same compile-path pattern). Each is a pinnable error example.
|
commit (no behavior change yet — `lowerAsmExpr` keeps bailing). Then C.1 wires
|
||||||
- **C.0 (IR op, unlocks the rest):** add `inline_asm: InlineAsm` to `Op`
|
`lowerAsmExpr` to actually intern strings + lower input `Ref`s + build the op and
|
||||||
(`src/ir/inst.zig`) + interp `bailDetail`; then `lowerAsmExpr` stops bailing
|
compute the result `TypeId`, at which point **result-type derivation becomes
|
||||||
and builds the op, at which point **result-type derivation becomes observable**
|
observable** and the auto-naming/tuple typing (`expr_typer.zig`, deferred from B)
|
||||||
(so the auto-naming rule + tuple result typing in `expr_typer.zig` can be
|
can be tested end-to-end. See `PLAN-ASM.md` Phase C + design §II.6.
|
||||||
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.
|
Remaining deferred validation (do alongside C, once template scanning is worth
|
||||||
(Result-type derivation in `expr_typer.zig` is intentionally deferred until C
|
it): `%[name]` references in the template that name no operand. Needs effective-
|
||||||
makes it observable — see Current state.)
|
name resolution (explicit `[name]` ∪ auto-derived register names).
|
||||||
|
|
||||||
## Log
|
## Log
|
||||||
- (init) Plan + design doc written; ASM stream opened.
|
- (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
|
no-output⇒volatile, with named diagnostics before the codegen bail. Locked with
|
||||||
1641 (volatile error) + 1642 (volatile accepted). `zig build test` green (650
|
1641 (volatile error) + 1642 (volatile accepted). `zig build test` green (650
|
||||||
corpus, 445 unit).
|
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
|
## Known issues
|
||||||
- **0137** — `sx run` on a program with no `main` segfaults (unguarded JIT entry
|
- **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
|
/// Inline assembly lowering. Phase B (partial): validate the asm shape in the
|
||||||
/// compile path with specific named diagnostics, THEN bail on the not-yet-
|
/// 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 +
|
/// 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");
|
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.
|
// `volatile` — otherwise its effects could be deleted. Mirrors Zig's rule.
|
||||||
var n_outputs: usize = 0;
|
var n_outputs: usize = 0;
|
||||||
for (ae.operands) |op| {
|
for (ae.operands) |op| {
|
||||||
|
|||||||
Reference in New Issue
Block a user