From 3a062780f7669a3444c761feba7a172acd4582eb Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 16 Jun 2026 22:59:49 +0300 Subject: [PATCH] issue(0140): comptime type-construction bail panics instead of diagnosing A failing declare/define (e.g. empty variant list) bails correctly in the interp, but evalComptimeType swallows last_bail_detail via `catch return null`; the decl poisons to .unresolved with no diagnostic and reaches LLVM emission -> panic ("unresolved type reached LLVM emission"), or hides behind a misleading downstream cascade. Pre-existing (plain define path), surfaced while starting the make_enum step. Blocks make_enum's computed (pointer-backed) []EnumVariant slice decode. Repro + investigation prompt filed; CHECKPOINT-METATYPE marked BLOCKED. Session paused pending fix per CLAUDE.md IMPASSABLE rule. --- current/CHECKPOINT-METATYPE.md | 23 +++- ...type-construction-bail-unresolved-panic.md | 127 ++++++++++++++++++ ...type-construction-bail-unresolved-panic.sx | 20 +++ 3 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 issues/0140-comptime-type-construction-bail-unresolved-panic.md create mode 100644 issues/0140-comptime-type-construction-bail-unresolved-panic.sx diff --git a/current/CHECKPOINT-METATYPE.md b/current/CHECKPOINT-METATYPE.md index b08a450a..679eada2 100644 --- a/current/CHECKPOINT-METATYPE.md +++ b/current/CHECKPOINT-METATYPE.md @@ -74,10 +74,15 @@ in the always-loaded prelude interns them into every module's type table and shifts every `.ir` snapshot. On-demand import keeps the prelude clean. ## Next step -Pick any (independent): +**BLOCKED on issue 0140** for the make_enum path (see Known issues). The other +two are unblocked: - **`make_enum(variants: []EnumVariant)`** sx helper over a COMPUTED (non-literal) variant list — exercises the interpreter decoding a value-arg slice in `define` - (vs. the literal `.[ … ]` the current examples use). + (vs. the literal `.[ … ]` the current examples use). **BLOCKED:** a computed + (pointer-backed) `[]EnumVariant` slice from a local var / `List` does not decode + in `defineEnum`/`decodeVariantElements`, and the resulting interp bail is + SWALLOWED (panic / misleading cascade, no root reason) — issue 0140. Fix 0140 + first so the decode failure surfaces cleanly, THEN implement the slice decode. - **Widen `type_info` / `TypeInfo` past `` `enum ``** — struct/tuple variants: add `` .`struct ``/`` .`tuple `` to the `TypeInfo` enum in `meta.sx`, teach `reflectTypeInfo` to build them, and teach `defineEnum` (→ a `defineType`) to @@ -87,9 +92,17 @@ Pick any (independent): self-reference already rejected — issue 0139.) ## Known issues -None. (issue 0139 — by-value self-reference segfault — RESOLVED: `checkInfiniteSize` -Pass 1g emits a loud "infinitely sized" diagnostic + breaks the cycle; covers -source + comptime types; `examples/1178` locks it.) +- **issue 0140 (OPEN) — a failing comptime type construction panics instead of + diagnosing.** `evalComptimeType` (`comptime.zig:~457`) swallows the interp's + `last_bail_detail` via `catch return null`; callers poison to `.unresolved` + with no diagnostic, and `.unresolved` reaches LLVM emission → panic ("unresolved + type reached LLVM emission"), or rides behind a misleading downstream cascade. + Pre-existing (plain `define` path; surfaced while starting make_enum). Repro: + `issues/0140-*.sx`. Blocks the make_enum computed-slice step. **Session paused + pending the fix** per the CLAUDE.md IMPASSABLE rule. +- issue 0139 — by-value self-reference segfault — RESOLVED (`checkInfiniteSize` + Pass 1g emits a loud "infinitely sized" diagnostic + breaks the cycle; + `examples/1178` locks it). ## Log - **`type_info($T)` reflection done (enum round-trip).** New `BuiltinId.type_info`; diff --git a/issues/0140-comptime-type-construction-bail-unresolved-panic.md b/issues/0140-comptime-type-construction-bail-unresolved-panic.md new file mode 100644 index 00000000..51ed1acb --- /dev/null +++ b/issues/0140-comptime-type-construction-bail-unresolved-panic.md @@ -0,0 +1,127 @@ +# 0140 — a failing comptime type construction panics ("unresolved type reached LLVM emission") instead of diagnosing the bail + +## Symptom + +One-line: when a comptime type construction (`declare`/`define`) bails in the +interpreter, the failure is swallowed — the decl is poisoned to `.unresolved` +with **no diagnostic**, and that `.unresolved` reaches LLVM emission and panics +instead of emitting a clean, build-gating error that names the bail reason. + +- **Observed:** `thread … panic: unresolved type reached LLVM emission — a type + resolution failure was not diagnosed/aborted` + (`src/backend/llvm/types.zig:176`, the `.unresolved` arm of `toLLVMTypeInfo`), + reached from `emitAlloca` (`src/backend/llvm/ops.zig:329`) for the local + `e : Empty = ---`. Exit 134 (panic), not a diagnostic. +- **Expected:** a build-time `.err` at the construction site carrying the + interpreter's bail detail — `defineEnum` already produces the precise reason + ("comptime define(): enum has no variants") via `bailDetail`, which sets + `Interpreter.last_bail_detail`. Exit 1, no panic, message visible to the user. + +This is **PRE-EXISTING** and orthogonal to the METATYPE `type_info` work that +surfaced it: the repro uses only the plain `define` path with an empty literal +variant list (`type_info` is not involved). It reproduces for *any* comptime +construction that bails — bad/empty `TypeInfo`, a `variants` value the decoder +can't read (e.g. a pointer-backed `[]EnumVariant` slice built from a local +variable / `List`, which is the next thing the make_enum step needs), etc. + +## Reproduction + +Minimal, standalone (only `modules/std.sx` + `modules/std/meta.sx`): + +```sx +#import "modules/std.sx"; +#import "modules/std/meta.sx"; + +Empty :: define(declare("Empty"), .enum(.{ variants = .[] })); + +main :: () -> i32 { + e : Empty = ---; + return 0; +} +``` + +Run: `./zig-out/bin/sx run issues/0140-comptime-type-construction-bail-unresolved-panic.sx` +→ panics today (exit 134); the fix should emit a diagnostic naming the bail +reason and exit 1 (no panic). + +### Bisection (what does / does not trigger the *panic*) + +| Variant | Result | +|---|---| +| `Empty :: define(declare("Empty"), .enum(.{ variants = .[] }))` + a *local* `e : Empty` | **PANICS** (exit 134) | +| same construction, but `Empty` is only *referenced as a type* (no value created) | poisons silently — often a confusing downstream cascade, no root reason | +| a make_enum-style computed slice `define(declare(n), .enum(.{ variants = local_slice }))` then a `.variant` *literal* | exit 1 with `"cannot infer enum type for '.x'"` — the literal-inference error fires first and *incidentally* gates emission, so no panic, but the **real bail reason is still never shown** | + +So the panic specifically needs the `.unresolved` type to survive to emission +(here via a local `alloca`); when some *other* diagnostic happens to fire first, +the build aborts before emission and the panic is dodged — but in **every** case +the actual interp bail reason (`last_bail_detail`) is lost and the user sees +either a panic or a misleading follow-on, never the root cause. + +## Investigation prompt + +> A comptime type construction (`declare`/`define`, and reflection like +> `type_info`) that bails in the interpreter is swallowed: the build either +> panics at LLVM emission ("unresolved type reached LLVM emission") or shows a +> misleading downstream cascade, instead of a clean diagnostic naming the bail +> reason. Repro: +> `issues/0140-comptime-type-construction-bail-unresolved-panic.sx` (panics, +> exit 134 today; the fix should print a diagnostic with the bail reason and +> exit 1 — no panic). +> +> Root area: `evalComptimeType` in `src/ir/lower/comptime.zig` (~line 457): +> +> ```zig +> const result = interp.call(func_id, &.{}) catch return null; +> return result.asTypeId(); +> ``` +> +> The `catch return null` drops the interpreter's `last_bail_detail` +> (`Interpreter.last_bail_detail`, `src/ir/interp.zig:218`, set by every +> `bailDetail(...)` — e.g. `defineEnum`'s "enum has no variants"). The two +> callers then poison to `.unresolved` with NO diagnostic: +> - `src/ir/lower/decl.zig:777`: `const tid = self.evalComptimeType(cd.value) orelse TypeId.unresolved;` +> then `putTypeAlias(..., .unresolved)`. +> - `src/ir/lower/generic.zig:1762`: `orelse return .unresolved`. +> +> `.unresolved` is the correct *sentinel* (it trips the codegen tripwire so a +> resolution failure can never silently ship), but here NOTHING converts it into +> a user-facing diagnostic, so it either crashes at emit or rides along behind a +> follow-on error. +> +> Suspected fix: in `evalComptimeType`, on the interp error path, emit a +> diagnostic at the construction expression's span carrying `last_bail_detail` +> (mirror how `src/ir/emit_llvm.zig:856` / `:933` already surface +> `last_bail_detail` for `#run` / comptime-function evaluation — +> `Interpreter.last_bail_detail orelse ""`). Reset +> `last_bail_detail = null` before the call and read it after the `catch`. Return +> `.unresolved` (keep the poison) *after* the diagnostic has been emitted, so the +> build is gated with a real message and no `.unresolved` reaches emission +> unannounced. Make sure BOTH callers (decl + generic) route through the +> diagnostic — ideally emit inside `evalComptimeType` so neither caller can +> forget. Do NOT swap `.unresolved` for a "reasonable-looking" default type +> (per CLAUDE.md REJECTED PATTERNS); the sentinel + diagnostic is the right shape. +> +> Verification: the repro emits a diagnostic naming the bail reason and exits 1 +> (no panic); then `zig build && zig build test` green. Pin the repro as an +> `11xx` diagnostics example (move to +> `examples/11xx-diagnostics-comptime-type-construction-bail.sx`, seed the +> `expected/*.exit` marker, capture with `-Dupdate-goldens`, review the diff). +> Also add a positive note in `current/CHECKPOINT-METATYPE.md` that the +> make_enum computed-slice step can proceed once this lands (the decoder's +> "variants is not a slice/array" bail will then be a clean diagnostic instead +> of a cascade/panic). + +## Notes + +- Tripwire site (symptom): `src/backend/llvm/types.zig:176` (`toLLVMTypeInfo`, + `.unresolved` arm) via `emitAlloca` (`src/backend/llvm/ops.zig:329`). +- Root area (cause): `evalComptimeType` `catch return null` + (`src/ir/lower/comptime.zig:~457`) drops `last_bail_detail`; callers + `decl.zig:777` / `generic.zig:1762` poison to `.unresolved` with no diagnostic. +- The interpreter already computes the precise reason — `bailDetail` / + `typeErrorDetail` set `Interpreter.last_bail_detail` (`interp.zig:218`); the + `#run` path at `emit_llvm.zig:856` is the existing precedent for surfacing it. +- Blocks: the make_enum computed-(non-literal)-variant-list step + (`current/PLAN-METATYPE.md` Status), whose decode failures currently land in + exactly this swallow. diff --git a/issues/0140-comptime-type-construction-bail-unresolved-panic.sx b/issues/0140-comptime-type-construction-bail-unresolved-panic.sx new file mode 100644 index 00000000..4a9d365d --- /dev/null +++ b/issues/0140-comptime-type-construction-bail-unresolved-panic.sx @@ -0,0 +1,20 @@ +// Repro for issue 0140 — a FAILING comptime type construction +// (`define` with an empty variant list) bails correctly in the interp +// ("comptime define(): enum has no variants"), but that bail is swallowed: +// `evalComptimeType` returns null, the decl is poisoned to `.unresolved` +// with NO diagnostic, and the `.unresolved` type reaches LLVM emission and +// PANICS ("unresolved type reached LLVM emission") instead of surfacing a +// clean error with the bail reason. +// +// Expected: a build-time diagnostic at the construction site naming the +// bail reason (e.g. "comptime type construction failed: enum has no +// variants"), exit 1, no panic. +#import "modules/std.sx"; +#import "modules/std/meta.sx"; + +Empty :: define(declare("Empty"), .enum(.{ variants = .[] })); + +main :: () -> i32 { + e : Empty = ---; + return 0; +}