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
4.0 KiB
0144 — unrecognized $T-param #builtin silently returns 0
RESOLVED. The generic monomorphization path (
monomorphizeFunction'sbuiltin_exprbody branch insrc/ir/lower/generic.zig) no longer falls through toensureTerminator's silentconstInt(0)whenresolveBuiltinreturns null for an unknown name. It now emits a louderror: 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)prints0, 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
#builtinwith a$T: Typeparameter silently lowers to0(exit 0) instead of erroring. Repro:mystery :: ($T: Type, x: T) -> T #builtin;thenprint("{}\n", mystery(i64, 42))prints0. Expected: a loud "unknown #builtin" diagnostic + non-zero exit (the non-type-param#builtinform 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$Targ / folds to a default0/.i64rather than rejecting. Trace where a bodiless#builtincall whose name matches NO recognizer (reflection / atomic intrinsics / etc.) ends up producing aconst_int 0(or anelse => .i64-style default inresolveTypeArgor the builtin-call lowering). The fix: when a call resolves to a bodiless#builtindecl that no recognizer claimed, emitself.diagnostics.addFmt(.err, callee.span, "unknown #builtin '{s}'", .{name})and return a distinct sentinel — never a silent0. Grep for the#builtinbody marker handling + how bodiless-decl calls are lowered. Verification: run the repro, expect the new diagnostic + non-zero exit; confirmzig build teststays green (no legitimate#builtinis newly rejected — every shipped builtin IS recognized, so only genuinely-unknown names should newly error). Add a11xxdiagnostic example locking the new error.