The `type_name` / `type_eq` reflection builtins resolved their Type arg's IR
type via `getRefIRType(...) orelse TypeId.s64`, then gated `== .any`. A failed
must-succeed lookup silently became `.s64` (`!= .any`), classifying a boxed
`Any` arg as bare i64 and reading the wrong value with no diagnostic.
Add the sibling classifier `LLVMEmitter.reflectArgRepr`, which routes the
lookup through `argIRTypeOrFail` (the issue-0074 `.unresolved` resolver) and
returns `{ boxed, bare, unresolved }`. The three emit sites in ops.zig
(`type_name` + `type_eq` x2) now switch on it: `.boxed` extracts the Any value
field, `.bare` uses the value directly, `.unresolved` hits a hard `@panic`
tripwire — never silently treated as bare. Real args always resolve, so the
happy path is byte-identical (suite stays 361/0, zero snapshot churn).
Secondary `lower.zig` `null_literal`/`undef_literal => target_type orelse .void`
confirmed intentional (typeless-literal default deliberately handled by
emitConstNull/emitConstUndef as null-ptr / undef-i64) — left with an invariant
comment, not the `.unresolved` tripwire.
Regression test in emit_llvm.test.zig asserts the loud path: fail-before with
`orelse .s64` yields `.bare`; pass-after yields `.unresolved`.
6.4 KiB
RESOLVED (2026-06-03) Root cause: the
type_name/type_eqreflection builtins resolved theirTypearg's IR type withgetRefIRType(...) orelse TypeId.s64, then gated== .any— so a failed must-succeed lookup silently became "bare i64" (.s64 != .any), reading the wrong value with no diagnostic. Fix: added the sibling classifierLLVMEmitter.reflectArgRepr(src/ir/emit_llvm.zig) which routes the lookup throughargIRTypeOrFail→.unresolvedand returns{ boxed, bare, unresolved }. The three emit sites (src/backend/llvm/ops.zigtype_name+type_eq×2) nowswitchon it:.boxedextracts theAnyvalue field,.bareuses the value directly, and.unresolvedhits a hard@panictripwire — never silently classified as bare. Happy path (real args always resolve) is byte-identical; suite stays 361/0. Secondary (confirmed intentional):src/ir/lower.zig:2531/2532(null_literal/undef_literal→target_type orelse .void) is a typeless-literal default, not a lookup-swallow —emitConstNull/emitConstUndefdeliberately handle.void(null-ptr / undef-i64). Left in place with an invariant comment. Regression test:src/ir/emit_llvm.test.zig— "emit: reflectArgRepr surfaces .unresolved for an unresolvable reflection arg ref (issue 0075)" (fail-before withorelse .s64→.bare; pass-after →.unresolved).
0075 — silent getRefIRType(...) orelse TypeId.s64 fallback in reflection builtins
Symptom
One-line: The type_name and type_eq reflection builtins resolve their Type
argument's IR type via getRefIRType(...) orelse TypeId.s64 — the forbidden
silent-type-lookup fallback (.s64 is the exact issue-0042 sentinel the project
rules name) — so a failed must-succeed lookup silently decides "not boxed (!= .any)"
and mis-handles the value with no diagnostic.
Observed (primary — must fix): self.e.getRefIRType(...) orelse TypeId.s64 at:
src/backend/llvm/ops.zig:1023(.type_namebuiltin —arg_ir_ty, gates the== .anyboxed-extract vs bare-i64 decision)src/backend/llvm/ops.zig:1049(.type_eqbuiltin — first operand)src/backend/llvm/ops.zig:1055(.type_eqbuiltin — second operand)
getRefIRType (src/ir/emit_llvm.zig:2229, ?TypeId) returns null only when a ref
is neither a function param nor a block instruction result — a must-not-happen case
for a real builtin argument. On null the code defaults to .s64, then tests
arg_ir_ty == .any; the .s64 default silently means "treat as a bare TypeId index,
not a boxed Any", so a genuinely-boxed arg whose lookup failed would skip the
ExtractValue and use the wrong value — silent miscompile, no diagnostic.
Expected: per CLAUDE.md REJECTED PATTERNS, a failed must-succeed type lookup
surfaces a diagnostic / hard tripwire (e.g. the .unresolved sentinel introduced for
issue 0074), never a real-type default.
Secondary (confirm — borderline)
src/ir/lower.zig:2527—.null_literal => constNull(self.target_type orelse .void)src/ir/lower.zig:2528—.undef_literal => constUndef(self.target_type orelse .void)target_typeis a context hint that may be legitimately absent for a barenull/undefwith no expected type — this may be an INTENTIONAL default rather than a lookup-swallow. The fix session should confirm: if anull/undefliteral reaching here without atarget_typeis actually a must-not-happen case, make it loud; if a typeless null/undef is legitimate, leave it and add a one-line comment stating the invariant.
Audited — intentional language defaults (NO action; documented so they aren't re-flagged)
src/ir/lower.zig:4855—int_literal => constInt(lit.value, info.ty orelse .s64): an untyped integer literal defaulting tos64is standard language semantics, not a lookup failure.src/ir/lower.zig:4856—float_literal => constFloat(..., info.ty orelse .f64): untyped float literal defaults tof64— language semantics.src/ir/type_bridge.zig:334—.tag_type = tag_type orelse .s64: documented ("enum unions are always tagged (default i64)") — an intentional default tag type, not a swallowed lookup.
Provenance / scope
Pre-existing, NOT introduced by the arch-refactor. Discovered during the issue-0074
fix (the fix worker surfaced the reflection .s64 fallbacks as a separate pattern
outside 0074's FFI-arg scope) and confirmed by a manager sweep
(rg "orelse \.(s64|void|...)" src). Filed per the IMPASSIBLE RULE (existing
default-returns that swallow a lookup failure → file, don't fix in place).
Reproduction
Latent / static (same nature as 0074): well-formed IR always gives a builtin arg a
resolvable type, so the .s64 default can't be driven at runtime today — which is why
it's dangerous (a future IR change would silently miscompile type_name/type_eq).
Exercised by the comptime/reflection examples; the fix must keep the suite at 361/0.
Investigation prompt (ready to paste into a fresh session)
In
/Users/agra/projects/sx, the.type_nameand.type_eqreflection builtins insrc/backend/llvm/ops.zig(lines 1023, 1049, 1055) resolve a Type argument's IR type with the forbidden silent fallbackgetRefIRType(...) orelse TypeId.s64, used to gate a== .anyboxed-vs-bare decision. Issue 0074 already added the shared resolverLLVMEmitter.argIRTypeOrFail(src/ir/emit_llvm.zig) returning the dedicated.unresolvedsentinel on a failed lookup. Route these three sites through that helper (or a sibling) so a failed lookup yields.unresolved— never.s64; then==.anyis false for.unresolvedAND you must make the unresolved case loud (diagnostic viaself.diagnostics.addFmt(.err, span, ...)or a hard tripwire), not silently "bare i64". Also resolve the borderlinelower.zig:2527/2528target_type orelse .void(confirm intentional vs make-loud; comment the invariant either way). Leave the audited intentional defaults (lower.zig:4855/4856,type_bridge.zig:334) untouched. Verify:/Users/agra/.zvm/bin/zig build && /Users/agra/.zvm/bin/zig build test && bash tests/run_examples.shstays 361/0; add a*.test.zigregression test asserting the loud.unresolvedpath for atype_name/type_eqarg with an unresolvable ref (fail-before/pass-after). Expected new behavior: an unresolved reflection-builtin arg type surfaces loudly, never silently defaults to.s64.