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:
79
issues/0144-unrecognized-type-param-builtin-silent-zero.md
Normal file
79
issues/0144-unrecognized-type-param-builtin-silent-zero.md
Normal 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.
|
||||||
Reference in New Issue
Block a user