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:
30
examples/optionals/0912-null-coalesce-struct-literal.sx
Normal file
30
examples/optionals/0912-null-coalesce-struct-literal.sx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Struct literal as the default of `??` (null-coalesce).
|
||||||
|
// Regression (issue 0166): a bare `.{ ... }` default was lowered with no
|
||||||
|
// target type, so its `struct_init.ty` stayed `.unresolved` and panicked at
|
||||||
|
// LLVM emission ("unresolved type reached LLVM emission"). The fix threads the
|
||||||
|
// optional's child type `T` into the RHS lowering, so the literal resolves to
|
||||||
|
// `T`. Covers both runtime paths: default TAKEN (lhs null) and NOT taken (lhs
|
||||||
|
// present), plus a nested struct-literal default and an all-defaulted `.{}`.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Inner :: struct { x: i64 = 0; }
|
||||||
|
T :: struct { a: i64 = 7; b: i64 = 3; inner: Inner; }
|
||||||
|
|
||||||
|
mk :: (give: bool) -> ?T {
|
||||||
|
if give { return .{ a = 1, b = 2, inner = .{ x = 4 } }; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
// Default NOT taken — lhs has a value.
|
||||||
|
present := mk(true) ?? .{ a = 9, inner = .{ x = 99 } };
|
||||||
|
print("{} {} {}\n", present.a, present.b, present.inner.x);
|
||||||
|
|
||||||
|
// Default TAKEN — lhs is null, struct-literal default selected.
|
||||||
|
absent := mk(false) ?? .{ a = 9, inner = .{ x = 99 } };
|
||||||
|
print("{} {} {}\n", absent.a, absent.b, absent.inner.x);
|
||||||
|
|
||||||
|
// All-defaulted `.{}` default.
|
||||||
|
empty := mk(false) ?? .{};
|
||||||
|
print("{} {} {}\n", empty.a, empty.b, empty.inner.x);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
1 2 4
|
||||||
|
9 3 99
|
||||||
|
7 3 0
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
# 0166 — `?? .{ ... }` struct-literal default panics with "unresolved type reached LLVM emission"
|
# 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
|
## Symptom
|
||||||
|
|
||||||
Using a struct literal as the default of a `??` (null-coalesce) operator panics:
|
Using a struct literal as the default of a `??` (null-coalesce) operator panics:
|
||||||
|
|||||||
43
issues/0172-null-coalesce-non-optional-lhs-panics.md
Normal file
43
issues/0172-null-coalesce-non-optional-lhs-panics.md
Normal 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.
|
||||||
@@ -1871,7 +1871,16 @@ pub fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref {
|
|||||||
|
|
||||||
// RHS block: evaluate fallback and branch to merge
|
// RHS block: evaluate fallback and branch to merge
|
||||||
self.builder.switchToBlock(rhs_bb);
|
self.builder.switchToBlock(rhs_bb);
|
||||||
|
// Thread the optional's child type as the expected/target type so an
|
||||||
|
// untyped struct literal default (`?? .{ ... }`) resolves to `T` rather
|
||||||
|
// than staying `.unresolved` and reaching codegen as a malformed
|
||||||
|
// struct_init (issue 0166). Scalar/pointer/typed defaults are unaffected:
|
||||||
|
// they ignore `target_type` or coerce identically. Restore afterwards so
|
||||||
|
// a `??` nested inside a larger expression doesn't leak this target type.
|
||||||
|
const saved_tt = self.target_type;
|
||||||
|
self.target_type = inner_ty;
|
||||||
var rhs = self.lowerExpr(nc.rhs);
|
var rhs = self.lowerExpr(nc.rhs);
|
||||||
|
self.target_type = saved_tt;
|
||||||
const rhs_ty = self.builder.getRefType(rhs);
|
const rhs_ty = self.builder.getRefType(rhs);
|
||||||
if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) {
|
if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) {
|
||||||
rhs = self.coerceToType(rhs, rhs_ty, inner_ty);
|
rhs = self.coerceToType(rhs, rhs_ty, inner_ty);
|
||||||
|
|||||||
Reference in New Issue
Block a user