Files
sx/issues/0075-reflection-builtin-i64-type-fallback.md
agra d8076b9333 lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.

Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).

Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.

zig build test: 426/426; examples suite: 595/595.
2026-06-12 09:31:53 +03:00

6.4 KiB
Raw Permalink Blame History

RESOLVED (2026-06-03) Root cause: the type_name / type_eq reflection builtins resolved their Type arg's IR type with getRefIRType(...) orelse TypeId.i64, then gated == .any — so a failed must-succeed lookup silently became "bare i64" (.i64 != .any), reading the wrong value with no diagnostic. Fix: added the sibling classifier LLVMEmitter.reflectArgRepr (src/ir/emit_llvm.zig) which routes the lookup through argIRTypeOrFail.unresolved and returns { boxed, bare, unresolved }. The three emit sites (src/backend/llvm/ops.zig type_name + type_eq ×2) now switch on it: .boxed extracts the Any value field, .bare uses the value directly, and .unresolved hits a hard @panic tripwire — 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_literaltarget_type orelse .void) is a typeless-literal default, not a lookup-swallow — emitConstNull/emitConstUndef deliberately 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 with orelse .i64.bare; pass-after → .unresolved).

0075 — silent getRefIRType(...) orelse TypeId.i64 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.i64 — the forbidden silent-type-lookup fallback (.i64 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.i64 at:

  • src/backend/llvm/ops.zig:1023 (.type_name builtin — arg_ir_ty, gates the == .any boxed-extract vs bare-i64 decision)
  • src/backend/llvm/ops.zig:1049 (.type_eq builtin — first operand)
  • src/backend/llvm/ops.zig:1055 (.type_eq builtin — 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 .i64, then tests arg_ir_ty == .any; the .i64 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_type is a context hint that may be legitimately absent for a bare null/undef with no expected type — this may be an INTENTIONAL default rather than a lookup-swallow. The fix session should confirm: if a null/undef literal reaching here without a target_type is 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:4855int_literal => constInt(lit.value, info.ty orelse .i64): an untyped integer literal defaulting to i64 is standard language semantics, not a lookup failure.
  • src/ir/lower.zig:4856float_literal => constFloat(..., info.ty orelse .f64): untyped float literal defaults to f64 — language semantics.
  • src/ir/type_bridge.zig:334.tag_type = tag_type orelse .i64: 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 .i64 fallbacks as a separate pattern outside 0074's FFI-arg scope) and confirmed by a manager sweep (rg "orelse \.(i64|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 .i64 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_name and .type_eq reflection builtins in src/backend/llvm/ops.zig (lines 1023, 1049, 1055) resolve a Type argument's IR type with the forbidden silent fallback getRefIRType(...) orelse TypeId.i64, used to gate a == .any boxed-vs-bare decision. Issue 0074 already added the shared resolver LLVMEmitter.argIRTypeOrFail (src/ir/emit_llvm.zig) returning the dedicated .unresolved sentinel on a failed lookup. Route these three sites through that helper (or a sibling) so a failed lookup yields .unresolved — never .i64; then ==.any is false for .unresolved AND you must make the unresolved case loud (diagnostic via self.diagnostics.addFmt(.err, span, ...) or a hard tripwire), not silently "bare i64". Also resolve the borderline lower.zig:2527/2528 target_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.sh stays 361/0; add a *.test.zig regression test asserting the loud .unresolved path for a type_name/type_eq arg with an unresolvable ref (fail-before/pass-after). Expected new behavior: an unresolved reflection-builtin arg type surfaces loudly, never silently defaults to .i64.