Files
sx/issues/0140-comptime-type-construction-bail-unresolved-panic.md
agra 3a062780f7 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.
2026-06-16 22:59:49 +03:00

6.7 KiB

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):

#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):

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.