Files
sx/issues/0144-unrecognized-type-param-builtin-silent-zero.md
agra ad45ae07ef fix: diagnose unknown generic #builtin instead of silently returning 0 (issue 0144)
A bodiless #builtin with a $T: Type param routes through monomorphization.
When resolveBuiltin returned null for an unrecognized name, the builtin-body
branch fell through to ensureTerminator's constInt(0) -- a silent-fallback
default the CLAUDE.md REJECTED PATTERNS forbid. Emit a loud
'error: unknown #builtin <name>' diagnostic instead.

Regression: examples/1189-diagnostics-unknown-builtin.sx
2026-06-21 09:10:38 +03:00

4.0 KiB

0144 — unrecognized $T-param #builtin silently returns 0

RESOLVED. The generic monomorphization path (monomorphizeFunction's builtin_expr body branch in src/ir/lower/generic.zig) no longer falls through to ensureTerminator's silent constInt(0) when resolveBuiltin returns null for an unknown name. It now emits a loud error: unknown #builtin '<name>' diagnostic — removing exactly the silent-fallback-default the CLAUDE.md REJECTED PATTERNS forbid. Regression test: examples/1189-diagnostics-unknown-builtin.sx.

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 #builtins ran to 0 with exit 0 instead of erroring.

Reproduction

Standalone (only needs modules/std.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):

#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.