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.
This commit is contained in:
agra
2026-06-16 22:59:49 +03:00
parent 52b0dc2a9a
commit 3a062780f7
3 changed files with 165 additions and 5 deletions

View File

@@ -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`;

View File

@@ -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 "<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.

View File

@@ -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;
}