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).
59 lines
2.5 KiB
Markdown
59 lines
2.5 KiB
Markdown
# 0180 — `??` lowering defects for generic / alias / tuple optional lhs (wrong fallback, segfault, PHI mismatch)
|
|
|
|
## Symptom
|
|
|
|
`lowerNullCoalesce` mishandles several non-trivial optional lhs shapes (all with a
|
|
genuinely-optional lhs, so the issue-0172 non-optional guard correctly does not
|
|
fire). Found during adversarial review of issue 0172.
|
|
|
|
1. **Generic `??` returns the WRONG fallback** (VERIFIED — silent miscompile):
|
|
a `??` inside a generic function where the lhs is a type-param-typed optional
|
|
`?T` drops the RHS default and returns the zero payload instead.
|
|
2. **Alias-optional with a struct-literal default segfaults** (reported by review):
|
|
`?Opt ?? Opt.{}` where `Opt :: ?Struct` crashes in type interning
|
|
(`hashString`/wyhash).
|
|
3. **`?(i32) ?? i32` LLVM PHI-type mismatch** (reported by review): an
|
|
optional-of-1-tuple coalesced with a scalar default emits `phi { i32 }` vs
|
|
`i32`.
|
|
|
|
(Scalar-default alias-optional `Opt :: ?i64; o ?? 7` works correctly — the
|
|
defects are specific to the shapes above.)
|
|
|
|
## Reproduction
|
|
|
|
(1) Generic `??` wrong fallback — VERIFIED, prints `0`, expected `99`:
|
|
```sx
|
|
#import "modules/std.sx";
|
|
unwrap_or :: (d: $T, x: ?T) -> T { return x ?? d; }
|
|
main :: () { b : ?i32 = null; print("{}\n", unwrap_or(99, b)); } // prints 0 — WRONG
|
|
```
|
|
|
|
(2) Alias-optional + struct-literal default (reported segfault):
|
|
```sx
|
|
#import "modules/std.sx";
|
|
S :: struct { a: i64 = 0; }
|
|
Opt :: ?S;
|
|
main :: () { o : Opt = null; x := o ?? S.{ a = 7 }; print("{}\n", x.a); }
|
|
```
|
|
|
|
(3) Optional-of-tuple + scalar default (reported PHI mismatch):
|
|
```sx
|
|
#import "modules/std.sx";
|
|
main :: () { o : ?(i32) = null; x := o ?? 5; }
|
|
```
|
|
|
|
## Investigation prompt
|
|
|
|
`src/ir/lower/expr.zig` `lowerNullCoalesce`. For (1), when the lhs optional's
|
|
child is a TYPE PARAMETER (`?T`, resolved per monomorphization), the present/
|
|
absent merge appears to drop the RHS default and yield the zero payload — check
|
|
that `inner_ty` and the merge-block param resolve to the monomorphized child and
|
|
that the RHS (`d`) is correctly selected on the null path. For (2)/(3), the merge
|
|
of present-payload vs default has a type mismatch when the child is an
|
|
alias/struct (interning crash) or a 1-tuple (PHI `{i32}` vs `i32`) — the default
|
|
and the unwrapped payload must share the exact merge TypeId. Verify each repro
|
|
produces the expected value (`99`, `7`, and a clean `?(i32)` coalesce); confirm
|
|
the generic case across multiple instantiations. Add regressions under
|
|
`examples/optionals/09xx-...`. (These reproduce on master, independent of the
|
|
issue-0172 guard.)
|