issue 0144: unrecognized $T-param #builtin silently returns 0

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).
This commit is contained in:
agra
2026-06-20 14:09:41 +03:00
parent 9bcb4159ef
commit 3fad2d5a21

View File

@@ -0,0 +1,79 @@
# 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.