# 0144 — unrecognized `$T`-param `#builtin` silently returns 0 ## Symptom A **bodiless `#builtin`** whose signature takes a `$T: Type` parameter, when the compiler does **not** recognize its name, **silently evaluates to `0`** (process exit 0) instead of emitting a loud "unknown builtin" diagnostic. - **Observed:** `mystery(i64, 42)` prints `0`, exit 0. - **Expected:** a loud compile error naming the unrecognized builtin (e.g. `error: unknown #builtin 'mystery'`) + non-zero exit. Contrast: a bodiless `#builtin` WITHOUT a type parameter (e.g. `noret :: (x: i64) #builtin;`) is lowered as an ordinary external call and fails **loudly** at link time (`JIT session error: Symbols not found: [ _noret ]`). So the silent path is specific to the `$T: Type`-parametrized (reflection-builtin- shaped) form, which is routed through the reflection/type-arg lowering and falls through to a zero/`i64` default rather than being rejected. This is the exact class CLAUDE.md "REJECTED PATTERNS" forbids: a lookup that fails (here: "is this a recognized builtin?") returns a plausible-looking default (`0`) instead of surfacing the failure. It was discovered during the atomics stream — before `atomic_load`/`atomic_store` recognition existed, `Atomic($T)`'s methods calling those `#builtin`s ran to `0` with exit 0 instead of erroring. ## Reproduction Standalone (only needs `modules/std.sx`): ```sx #import "modules/std.sx"; // Bodiless #builtin the compiler does not recognize, with a `$T: Type` param. mystery :: ($T: Type, x: T) -> T #builtin; main :: () { print("mystery(42) = {}\n", mystery(i64, 42)); // prints 0, exit 0 } ``` Also reproduces through a generic-struct method (the original atomics shape): ```sx #import "modules/std.sx"; mystery :: ($T: Type, x: T) -> T #builtin; Box :: struct ($T: Type) { v: T; get :: (self: *Box(T)) -> T { return mystery(T, self.v); } } main :: () { b := Box(i64).{ v = 42 }; print("{}\n", b.get()); // prints 0, exit 0 } ``` ## Investigation prompt (paste into a fresh session) > Fix issue 0144: an unrecognized bodiless `#builtin` with a `$T: Type` parameter > silently lowers to `0` (exit 0) instead of erroring. Repro: > `mystery :: ($T: Type, x: T) -> T #builtin;` then `print("{}\n", mystery(i64, 42))` > prints `0`. Expected: a loud "unknown #builtin" diagnostic + non-zero exit (the > non-type-param `#builtin` form already fails loudly at link time, so only the > type-param/reflection-shaped path is silent). > > Suspected area: `src/ir/lower/call.zig` — the reflection-builtin recognition > (`tryLowerReflectionCall`) declines unknown names, and the call then falls > through a path that resolves the `$T` arg / folds to a default `0` / `.i64` > rather than rejecting. Trace where a bodiless `#builtin` call whose name matches > NO recognizer (reflection / atomic intrinsics / etc.) ends up producing a > `const_int 0` (or an `else => .i64`-style default in `resolveTypeArg` or the > builtin-call lowering). The fix: when a call resolves to a bodiless `#builtin` > decl that no recognizer claimed, emit > `self.diagnostics.addFmt(.err, callee.span, "unknown #builtin '{s}'", .{name})` > and return a distinct sentinel — never a silent `0`. Grep for the `#builtin` > body marker handling + how bodiless-decl calls are lowered. Verification: run the > repro, expect the new diagnostic + non-zero exit; confirm `zig build test` stays > green (no legitimate `#builtin` is newly rejected — every shipped builtin IS > recognized, so only genuinely-unknown names should newly error). Add a `11xx` > diagnostic example locking the new error.