feat(asm): portable symbol refs — auto-inject :c operand modifier

A `%[name]` that references a symbol ("s") operand without an explicit
modifier now lowers to `${N:c}` (LLVM 'bare constant — no punctuation')
instead of `${N}`. This makes `bl %[fn]` / `call %[fn]` portable across
targets with no per-arch knowledge: x86 would otherwise render `$cb`
(an invalid call target, requiring a hand-written `:P`); aarch64 is
unaffected. Verified `:c` is equivalent to `:P` for x86-64 calls (both
emit R_X86_64_PLT32), and correct for branch targets, RIP-relative
addressing, and `$`-prefixed absolute immediates.

renderAsmTemplate injects `:c` only for symbol operands lacking an
explicit modifier (asmNamedIsSymbol helper); an explicit `%[name:X]`
still wins (escape hatch). x86 example 1659 drops its `:P` for the same
plain `%[fn]` as aarch64 1656. Snapshots regen to `${N:c}`. zig build
test green (668 corpus, 446 unit).
This commit is contained in:
agra
2026-06-16 09:04:23 +03:00
parent 79042ab9ab
commit 066ba54346
4 changed files with 28 additions and 8 deletions

View File

@@ -1042,6 +1042,17 @@ pub const Ops = struct {
return null;
}
/// True if the operand named `name` (effective name) is a symbol operand.
/// Drives the auto-`:c` injection in `renderAsmTemplate` so `%[fn]` is
/// portable across targets.
fn asmNamedIsSymbol(self: Ops, a: InlineAsm, name: []const u8) bool {
const e = self.e;
for (a.operands) |op| {
if (op.name != .empty and std.mem.eql(u8, e.ir_mod.types.getString(op.name), name) and asmIsSymbol(e, op)) return true;
}
return false;
}
/// Rewrite the asm template into LLVM form. State machine over the bytes:
/// `$`→`$$`, `%%`→`%`, `%=`→`${:uid}`, `%[name]`→`${N}`, `%[name:mod]`→
/// `${N:mod}`. Port of `FuncGen.zig`'s template rewriter.
@@ -1085,6 +1096,14 @@ pub const Ops = struct {
if (modifier) |m| {
out.append(alloc, ':') catch unreachable;
out.appendSlice(alloc, m) catch unreachable;
} else if (self.asmNamedIsSymbol(a, name)) {
// A symbol operand referenced without an explicit
// modifier: inject `:c` (bare constant — no punctuation)
// so a direct `bl`/`call %[fn]` emits the plain symbol on
// EVERY target. Without it x86 prints `$sym` (a bad call
// target); aarch64 is unaffected. Keeps the template
// portable — the user never writes a per-arch `:P`/`:c`.
out.appendSlice(alloc, ":c") catch unreachable;
}
out.append(alloc, '}') catch unreachable;
i = close + 1;