A bodiless #builtin with a $T: Type parameter that no recognizer matches folds to 0 (exit 0) instead of erroring — while a non-type-param #builtin link-errors loudly. Discovered during the atomics stream (Atomic methods ran to 0 before recognition existed). The reflection/type-arg lowering path defaults instead of rejecting (REJECTED-PATTERNS silent-fallback class). Repro + investigation prompt in the issue. Open (unpinned — not added to the suite, since the repro currently exits 0 by the bug).
3.5 KiB
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)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.