feat(asm): symbol operands ("s") — direct call/branch to a function

A `"s"` input operand feeds a function/global symbol; the template's
%[name] emits the platform-mangled name, so `bl %[fn]` / `call %[fn]`
branches DIRECTLY to it (PC-relative, no register load — one fewer
indirection than register-indirect `blr`).

Lowering: an `"s"` input lowers its RHS normally (a function name →
`ptr @fn`); the rejection added last commit is removed. Emit: a symbol
operand is passed with its OWN llvm type (LLVMTypeOf) and no coercion —
the function value is a `ptr`, and the old coerce-to-register-int path
mistyped it and failed the verifier. New asmIsSymbol helper.

Verified on aarch64: examples/1656 (sx → asm → bl _cb → sx → 42); the
emitted asm is a direct `bl <_cb>` (objdump-confirmed), IR constraint
`...,s,...`(ptr @cb). Flipped 1656 from the rejection lock to a runnable
aarch64 example. zig build test green (665 corpus, 446 unit).
This commit is contained in:
agra
2026-06-16 08:24:53 +03:00
parent c187122531
commit 10f4137cbd
7 changed files with 64 additions and 46 deletions

View File

@@ -838,6 +838,19 @@ pub const Ops = struct {
}
for (a.operands) |op| {
if (op.role != .input) continue;
// Symbol operand (`"s"`): a function/global passed as a
// compile-time constant (its address) — pass the value with its
// OWN llvm type and NO coercion, so the template's `${N}` emits
// the platform-mangled symbol (a direct `bl _sym`). Coercing to
// a register int (the path below) mistypes it (a function value
// resolves to `ptr`) and fails the LLVM verifier.
if (asmIsSymbol(e, op)) {
const v = e.resolveRef(op.operand);
param_types[i] = c.LLVMTypeOf(v);
call_args[i] = v;
i += 1;
continue;
}
const raw_ty = e.argIRTypeOrFail(op.operand);
const llvm_ty = e.toLLVMType(raw_ty);
param_types[i] = llvm_ty;
@@ -1003,6 +1016,15 @@ pub const Ops = struct {
return std.mem.indexOfScalar(u8, s, '*') != null;
}
/// True if `op` is a symbol operand — constraint `"s"`. The operand is a
/// function/global passed as a compile-time constant (its address); the
/// template's `${N}` emits the platform-mangled symbol name, so a direct
/// `bl %[fn]` / `call %[fn]` branches straight to it.
fn asmIsSymbol(e: *LLVMEmitter, op: InlineAsm.AsmOperand) bool {
const s = e.ir_mod.types.getString(op.constraint);
return std.mem.eql(u8, s, "s");
}
/// The positional index of a named operand in the LLVM operand list
/// (outputs first, then inputs) — the `N` in `%[name]` → `${N}`. Lowering
/// guarantees every `%[name]` names an operand, so callers can assume a hit.