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).
This commit is contained in:
agra
2026-06-22 22:17:01 +03:00
parent 0bc8005b99
commit 2ea25e84ec
7 changed files with 98 additions and 0 deletions

View File

@@ -1,5 +1,16 @@
# 0166 — `?? .{ ... }` struct-literal default panics with "unresolved type reached LLVM emission"
> **RESOLVED.** The `??` RHS struct literal was lowered with no target type, so
> its `struct_init.ty` stayed `.unresolved` and reached `emitStructInit`. Fix:
> in `src/ir/lower/expr.zig` `lowerNullCoalesce`, save `self.target_type`, set it
> to `inner_ty` (the optional's resolved child) before lowering `nc.rhs`, and
> restore afterward (unconditional, leak-free — `lowerExpr` returns a plain
> `Ref`). Verified across struct/slice/enum/tuple/protocol/nested-optional/
> generic child types and present/absent branches by 3 adversarial reviews.
> Regression: `examples/optionals/0912-null-coalesce-struct-literal.sx`.
> (Adjacent pre-existing bug found + filed: 0172 — `??` on a NON-optional lhs
> panics; `lowerNullCoalesce` must diagnose it.)
## Symptom
Using a struct literal as the default of a `??` (null-coalesce) operator panics:

View File

@@ -0,0 +1,43 @@
# 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
```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.