Files
sx/issues/0172-null-coalesce-non-optional-lhs-panics.md
agra 58f97fff10 fix: diagnose ?? with a non-optional lhs instead of codegen panic (issue 0172)
lowerNullCoalesce fed resolveOptionalInner's .unresolved (returned for a
non-optional lhs) into the merge-block params / optionalUnwrap / RHS
target type, reaching codegen and panicking 'unresolved type reached
LLVM emission'. Guard: when inferExprType(nc.lhs) is a resolved
non-optional type, emit a located diagnostic and bail; an .unresolved
lhs (prior error) is excluded to avoid double-report. ?? is optional-only
per specs.md (error unions use or/catch), so rejecting a failable lhs is
correct; comptime panic closed too.

Regression: examples/diagnostics/1200-diagnostics-null-coalesce-non-optional.sx.
Verified by 3 adversarial reviews, suite 790/0. Filed adjacent bug 0180
(?? lowering defects for generic/alias/tuple optional lhs).
2026-06-23 03:31:58 +03:00

57 lines
2.9 KiB
Markdown

# 0172 — `??` with a non-optional left-hand side panics instead of diagnosing
> **RESOLVED.** `lowerNullCoalesce` fed `resolveOptionalInner`'s `.unresolved`
> (returned for a non-optional lhs) into the merge-block / `optionalUnwrap` / RHS
> target type → codegen panic. Fix (`src/ir/lower/expr.zig`): before computing
> `inner_ty`, if `inferExprType(nc.lhs)` is a RESOLVED non-optional type, emit a
> located diagnostic ("left operand of '??' must be an optional, but has type
> '<T>'") and bail; an `.unresolved` lhs (prior error) is excluded to avoid
> double-report. `??` is optional-only per specs.md (error unions use
> `or`/`catch`), so rejecting a failable lhs is correct. Comptime panic closed
> too. Verified by 3 adversarial reviews; suite 790/0. Regression:
> `examples/diagnostics/1200-diagnostics-null-coalesce-non-optional.sx`. (Adjacent
> pre-existing `??`-lowering defects found + filed: 0180 — generic / alias /
> tuple optional lhs.)
## 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
```sx
#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.