Files
sx/issues/0172-null-coalesce-non-optional-lhs-panics.md
agra 2ea25e84ec fix: thread optional child type into ?? struct-literal default (issue 0166)
The RHS of a null-coalesce was lowered with no target type, so a bare
struct literal default (x ?? .{ ... }) produced a struct_init with
.ty == .unresolved that panicked in emitStructInit. lowerNullCoalesce
now saves self.target_type, sets it to the optional's resolved child
before lowering nc.rhs, and restores it (leak-free). Verified across
struct/slice/enum/tuple/protocol/nested-optional/generic child types by
3 adversarial reviews.

Regression: examples/optionals/0912-null-coalesce-struct-literal.sx.
Filed adjacent pre-existing bug 0172 (?? on a non-optional lhs panics).
2026-06-22 22:17:01 +03:00

2.1 KiB

0172 — ?? with a non-optional left-hand side panics instead of diagnosing

Symptom

Using ?? where the left operand is NOT an optional panics the compiler: panic: unresolved type reached LLVM emission (in emitStructInit for a struct default, or generally), exit 134. ?? is defined to operate on an optional lhs; a non-optional lhs is malformed user input that must be a clean type error, not a crash. Pre-existing (reproduces independent of the issue 0166 fix).

Reproduction

#import "modules/std.sx";
T :: struct { a: i64 = 0; }
main :: () {
  x := 5 ?? .{ a = 1 };   // panic: unresolved type reached LLVM emission, exit 134
}

Also panics: 5 ?? 7 (scalar default), some_non_optional_struct ?? .{ a = 1 }, and nested mk() ?? (5 ?? .{ a = 3 }). Expected: a located diagnostic like error: left operand of '??' must be an optional, but has type 'i64', exit 1.

Investigation prompt

src/ir/lower/expr.zig lowerNullCoalesce: resolveOptionalInner (~expr.zig:1900) returns .unresolved when nc.lhs is not optional, and the function proceeds to feed that .unresolved into the merge-block params, optionalUnwrap, and the RHS target type — which then reaches codegen and panics. Add a guard: after inferring the lhs type, if it is not an optional (or resolveOptionalInner yields .unresolved for a resolved-but-non-optional lhs), emit self.diagnostics.addFmt(.err, nc.lhs.span, "left operand of '??' must be an optional, but has type '{s}'", .{ formatTypeName(lhs_ty) }) and bail (return a placeholder), mirroring the non-pointer .* deref diagnostic at lowerDerefExpr (~expr.zig:1839). Be careful to still allow the legitimate cases: optional lhs (incl. a?.b chains returning optional), and make sure an already-.unresolved lhs from a PRIOR error (undefined name) doesn't double-report (that path already diagnoses via name resolution).

Verify: 5 ?? .{a=1}, 5 ?? 7, non-optional-struct ?? ... all exit 1 with the diagnostic and no panic; existing optional ?? cases still work. Add an examples/diagnostics/11xx-null-coalesce-non-optional.sx negative regression.