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

@@ -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| {