Rewrote 20 issue writeups to the extern/runtime-class vocabulary (#foreign→extern, foreign_class_map→runtime_class_map, parseForeignClassDecl→parseRuntimeClassDecl, findForeignMethodInChain→findRuntimeMethodInChain, dedupeForeignSymbol→ dedupeExternSymbol, is_foreign_c_api→is_extern_c_api, stale filename refs to the renamed examples, foreign-class→runtime-class, bare foreign→extern). Renamed issues/0043-…-foreign-class-…→…-runtime-class-…. PHASE 9 COMPLETE — 9.4 GATE PASSES: zero 'foreign' across src/library/examples/ issues/docs/editors/specs/readme/CLAUDE, excluding only the SQLite API constant SQLITE_CONSTRAINT_FOREIGNKEY + vendored sqlite3.c/.h (upstream third-party). Suite green (644 corpus / 443 unit, 0 failed).
5.2 KiB
0074 — silent getRefIRType(arg_ref) orelse .void fallback in FFI call-arg lowering
✅ RESOLVED. Root cause: four FFI call-arg lowering loops resolved an argument's IR type via
getRefIRType(arg_ref) orelse .void— a silent fallback to the load-bearing real type.void, which downstreamtoLLVMType→abiCoerceParamType→coerceArgtreat as a legitimate (void-typed) extern argument, corrupting the call ABI with no diagnostic. Fix: one shared resolverLLVMEmitter.argIRTypeOrFail([src/ir/emit_llvm.zig]) returns the dedicated.unresolvedsentinel on a failed lookup — never.void/.i64— so the failure cannot masquerade as a real type and tripstoLLVMType's existing hard@panictripwire at the call site. All four sites ([src/ir/emit_llvm.zig] JNI constructor; [src/backend/llvm/ops.zig] objc_msgSend, JNI non-virtual, JNICall<Type>Method) now route through the helper. Happy path is byte-identical (every real arg already has a resolved type) — FFI examples stay green with zero snapshot churn. Regression test (fail-before/pass-after):src/ir/emit_llvm.test.zig— "argIRTypeOrFail surfaces .unresolved for an unresolvable FFI arg ref (issue 0074)".
Symptom
One-line: Four FFI call-arg lowering sites silently default a failed
argument-type lookup to .void — the forbidden silent-type-fallback anti-pattern
(.void as a failed-type-lookup sentinel), which would produce a void-typed
extern-call argument (wrong LLVM param type → silent ABI corruption) with no
diagnostic if the lookup ever fails.
Observed: self.getRefIRType(arg_ref) orelse .void at:
src/ir/emit_llvm.zig:2463src/backend/llvm/ops.zig:517(Obj-Cobjc_msgSendarg loop)src/backend/llvm/ops.zig:731(JNI non-virtual call arg loop)src/backend/llvm/ops.zig:761(JNICall<Type>Methodarg loop)
Each then does toLLVMType(raw_ty) → abiCoerceParamType → coerceArg, so a
.void fallback silently mis-types the extern-call argument.
Expected: getRefIRType returning null for a real call argument is a
"must-succeed lookup" failure (every arg is a valid param/instruction ref). Per
CLAUDE.md REJECTED PATTERNS — ".void is an UNACCEPTABLE sentinel for a failed
type lookup" — the lookup failure must surface as a diagnostic / hard tripwire, not
a silently-corrupted argument type.
Provenance / scope
Pre-existing pattern (the emit_llvm.zig site is original; the three ops.zig sites
were relocated verbatim, behavior-preserving by step A7.4c of the arch-refactor —
the flow reviewer/observer correctly approved the relocations as equivalence-preserving).
Surfaced by the A9.3 final fallback-audit of the arch-refactor stream. Not
introduced by the refactor; filed per the IMPASSIBLE RULE ("If you find an existing
default-return in the compiler that swallows a lookup failure, treat it as a
discovered bug — file an issue, do not just delete the default in place").
Reproduction
This is a latent / static finding: there is no known sx program that drives
getRefIRType to null for a valid extern-call argument (well-formed IR always
has a type for every arg ref), so it cannot currently be triggered at runtime — which
is exactly why it is dangerous (a future IR change that breaks the invariant would
corrupt FFI ABI silently). The code paths are exercised (and must stay green after
the fix) by the existing FFI examples, e.g.:
examples/13xx-ffi-objc-* # objc_msgSend arg lowering (ops.zig:517)
examples/14xx-ffi-jni-* # JNI Call<Type>Method / non-virtual (ops.zig:731/761)
(No new minimal repro .sx is meaningful for a latent defensive fallback; the fix is
verified by (a) the FFI suite staying green and (b) a unit test that asserts the new
loud-failure path, see below.)
Investigation prompt (ready to paste into a fresh session)
In
/Users/agra/projects/sx, four FFI call-arg lowering sites use the forbidden silent type-fallbackself.getRefIRType(arg_ref) orelse .void(src/ir/emit_llvm.zig:2463;src/backend/llvm/ops.zig:517,:731,:761).getRefIRType(src/ir/emit_llvm.zig:2229, returns?TypeId) yieldsnullonly when a ref is neither a function param nor a block instruction result — a must-not-happen case for a real call argument. Replace the silent.voiddefault with a loud failure that cannot be mistaken for a real type, perCLAUDE.mdREJECTED PATTERNS: emit a diagnostic viaself.diagnostics.addFmt(.err, span, "...", .{...})and/or a hard tripwire (@panic/bailDetail-style) naming the op and the bad ref — do NOT substitute another real type. Prefer a single shared helper (e.g.argIRTypeOrFail(arg_ref, span)) used by all four sites so the policy lives in one place. Then: (1)/Users/agra/.zvm/bin/zig build && /Users/agra/.zvm/bin/zig build test && bash tests/run_examples.shmust stay 361/0 with the FFI examples green (the happy path is unchanged); (2) add a*.test.zigunit test that constructs an FFI call op with a bogus arg ref and asserts the loud failure fires (not a.voidsilent default). Expected new behavior: an unresolved FFI arg type produces a clear compiler error / panic, never a void-typed extern argument.