Files
sx/issues/0140-comptime-type-construction-bail-unresolved-panic.md
agra 37ec3da8cb fix(0140): surface comptime type-construction bail as a diagnostic
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.
2026-06-17 04:31:38 +03:00

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

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.