# 0140 — a failing comptime type construction panics ("unresolved type reached LLVM emission") instead of diagnosing the bail > **RESOLVED (2026-06-17).** Root cause exactly as the investigation prompt > hypothesized: `evalComptimeType` (`src/ir/lower/comptime.zig`) did > `interp.call(...) catch return null`, dropping the interpreter's > `last_bail_detail`; callers poisoned to `.unresolved` with no diagnostic, so the > sentinel reached LLVM emission and tripped the codegen panic (or hid behind a > downstream cascade). Fix: clear `Interpreter.last_bail_detail` before the call, > and on the `catch` emit a build-gating `.err` at the construction expression's > span — `"comptime type construction failed: {detail}"` (mirroring the `#run` > surfacing at `emit_llvm.zig:856`) — then return null (keeping the `.unresolved` > poison, now gated by a real message). The empty-variants repro now prints > `comptime type construction failed: comptime define(): enum has no variants` > and exits 1 (no panic); make_enum-style computed-slice failures surface their > root reason at the construction site instead of only the `.green` cascade. > Regression test: > [examples/1179-diagnostics-comptime-type-construction-bail.sx](../examples/1179-diagnostics-comptime-type-construction-bail.sx). ## 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.