From 3fad2d5a2191a78622d31da0b61c9b11cc11e00f Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 20 Jun 2026 14:09:41 +0300 Subject: [PATCH] issue 0144: unrecognized $T-param #builtin silently returns 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- ...cognized-type-param-builtin-silent-zero.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 issues/0144-unrecognized-type-param-builtin-silent-zero.md diff --git a/issues/0144-unrecognized-type-param-builtin-silent-zero.md b/issues/0144-unrecognized-type-param-builtin-silent-zero.md new file mode 100644 index 00000000..25bc1dff --- /dev/null +++ b/issues/0144-unrecognized-type-param-builtin-silent-zero.md @@ -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.