# 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 "<generic>"`). 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.
