evalComptimeType did `interp.call(...) catch return null`, dropping the
interp's last_bail_detail; callers poisoned to .unresolved with no
diagnostic, so the sentinel reached LLVM emission and panicked
("unresolved type reached LLVM emission"), or hid behind a downstream
cascade.
Clear last_bail_detail before the call; on the catch emit a build-gating
.err at the construction expr's span ("comptime type construction
failed: {detail}", mirroring the #run surfacing in emit_llvm.zig), then
return null to keep the .unresolved poison — now gated by a real message
so no unresolved type reaches emission unannounced.
Empty-variant define now prints 'comptime define(): enum has no
variants' and exits 1 (no panic); make_enum-style computed-slice
failures show their root reason at the construction site.
7.8 KiB
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) didinterp.call(...) catch return null, dropping the interpreter'slast_bail_detail; callers poisoned to.unresolvedwith no diagnostic, so the sentinel reached LLVM emission and tripped the codegen panic (or hid behind a downstream cascade). Fix: clearInterpreter.last_bail_detailbefore the call, and on thecatchemit a build-gating.errat the construction expression's span —"comptime type construction failed: {detail}"(mirroring the#runsurfacing atemit_llvm.zig:856) — then return null (keeping the.unresolvedpoison, now gated by a real message). The empty-variants repro now printscomptime type construction failed: comptime define(): enum has no variantsand exits 1 (no panic); make_enum-style computed-slice failures surface their root reason at the construction site instead of only the.greencascade. Regression test: 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.unresolvedarm oftoLLVMTypeInfo), reached fromemitAlloca(src/backend/llvm/ops.zig:329) for the locale : Empty = ---. Exit 134 (panic), not a diagnostic. - Expected: a build-time
.errat the construction site carrying the interpreter's bail detail —defineEnumalready produces the precise reason ("comptime define(): enum has no variants") viabailDetail, which setsInterpreter.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):
#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 liketype_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:
evalComptimeTypeinsrc/ir/lower/comptime.zig(~line 457):const result = interp.call(func_id, &.{}) catch return null; return result.asTypeId();The
catch return nulldrops the interpreter'slast_bail_detail(Interpreter.last_bail_detail,src/ir/interp.zig:218, set by everybailDetail(...)— e.g.defineEnum's "enum has no variants"). The two callers then poison to.unresolvedwith NO diagnostic:
src/ir/lower/decl.zig:777:const tid = self.evalComptimeType(cd.value) orelse TypeId.unresolved;thenputTypeAlias(..., .unresolved).src/ir/lower/generic.zig:1762:orelse return .unresolved.
.unresolvedis 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 carryinglast_bail_detail(mirror howsrc/ir/emit_llvm.zig:856/:933already surfacelast_bail_detailfor#run/ comptime-function evaluation —Interpreter.last_bail_detail orelse "<generic>"). Resetlast_bail_detail = nullbefore the call and read it after thecatch. Return.unresolved(keep the poison) after the diagnostic has been emitted, so the build is gated with a real message and no.unresolvedreaches emission unannounced. Make sure BOTH callers (decl + generic) route through the diagnostic — ideally emit insideevalComptimeTypeso neither caller can forget. Do NOT swap.unresolvedfor 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 testgreen. Pin the repro as an11xxdiagnostics example (move toexamples/11xx-diagnostics-comptime-type-construction-bail.sx, seed theexpected/*.exitmarker, capture with-Dupdate-goldens, review the diff). Also add a positive note incurrent/CHECKPOINT-METATYPE.mdthat 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,.unresolvedarm) viaemitAlloca(src/backend/llvm/ops.zig:329). - Root area (cause):
evalComptimeTypecatch return null(src/ir/lower/comptime.zig:~457) dropslast_bail_detail; callersdecl.zig:777/generic.zig:1762poison to.unresolvedwith no diagnostic. - The interpreter already computes the precise reason —
bailDetail/typeErrorDetailsetInterpreter.last_bail_detail(interp.zig:218); the#runpath atemit_llvm.zig:856is the existing precedent for surfacing it. - Blocks: the make_enum computed-(non-literal)-variant-list step
(
current/PLAN-METATYPE.mdStatus), whose decode failures currently land in exactly this swallow.