Discovered during the 0074 fix + a codebase-wide silent-type-fallback sweep. getRefIRType(...) orelse TypeId.s64 at ops.zig:1023/1049/1055 (type_name/type_eq). Blocker; to be resolved before the arch-refactor stream closes.
5.0 KiB
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.