fix: presence-preserving optional->optional coercion (issue 0180)
The generic-?? wrong-fallback was not in lowerNullCoalesce: coercing ?A -> ?B (differing payload, e.g. the ?i32->?i64 call-arg coercion when instantiating unwrap_or(99, ?i32)) routed through .optional_wrap, which unconditionally unwrapped the source and re-wrapped as ALWAYS-PRESENT, so a null became present-zero everywhere (args, returns, field init, var-decl, ??). Add a CoercionPlan.optional_to_optional (conversions.zig) + a presence-preserving arm in coerceMode (coerce.zig): has_value -> present: unwrap+coerce-child+wrap-present; absent: constNull(dst); merge via a dst_ty block param. lowerVarDecl gains a !src_is_optional guard so an annotated x : ?B = <?A> routes through the same arm (also makes aggregate-payload var-decl ?[3]i64->?[]i64 / ?Concrete->?Protocol work). Alias-optional struct-literal default already works (grouping + 0166); a 1-tuple default ?(i32,) ?? 5 now emits a clean diagnostic instead of an LLVM PHI abort (no implicit scalar->1-tuple coercion per spec). Regressions: optionals/0916 (generic ??), 0917 (alias struct default), 0918 (var-decl optional->optional), diagnostics/1202 (1-tuple default) + a conversions.test.zig unit test. Verified by 3 adversarial reviews, suite 798/0.
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
// `??` default whose type can't match a 1-tuple optional payload is a clean
|
||||||
|
// diagnostic, not an LLVM PHI-type crash.
|
||||||
|
//
|
||||||
|
// Regression (issue 0180): `?(i32,) ?? 5` built a merge PHI of `{i32}` (the
|
||||||
|
// unwrapped 1-tuple payload) vs `i32` (the scalar default) and aborted the
|
||||||
|
// LLVM verifier. There is no implicit scalar->1-tuple coercion (a 1-tuple
|
||||||
|
// value is written `(5,)`), so the mismatch is now reported at the default.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
o : ?(i32,) = null;
|
||||||
|
x := o ?? 5; // default 'i64' vs payload '(i32,)' -> diagnostic
|
||||||
|
print("{}\n", x.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: '??' default has type 'i64', but the optional's payload is '(i32,)' (note: a 1-tuple value is written '(x,)' with a trailing comma)
|
||||||
|
--> examples/diagnostics/1202-diagnostics-null-coalesce-tuple-default.sx:12:15
|
||||||
|
|
|
||||||
|
12 | x := o ?? 5; // default 'i64' vs payload '(i32,)' -> diagnostic
|
||||||
|
| ^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Generic `??` preserves the optional's presence across monomorphization.
|
||||||
|
//
|
||||||
|
// Regression (issue 0180): a `??` inside a generic fn whose lhs is a
|
||||||
|
// type-param-typed optional `?T` used to drop the RHS default and return the
|
||||||
|
// zero payload — a null `?i32` arriving as the `?T` arg was silently coerced
|
||||||
|
// to the monomorphized `?i64` with the has-bit hardcoded to `true` (the
|
||||||
|
// optional→optional arg coercion unwrapped-then-rewrapped-present), so
|
||||||
|
// `x ?? d` saw a present zero and returned 0 instead of `d`.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Pt :: struct { x: i64 = 0; y: i64 = 0; }
|
||||||
|
|
||||||
|
unwrap_or :: (d: $T, x: ?T) -> T { return x ?? d; }
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
// ?i32: null -> default, present -> value.
|
||||||
|
bi : ?i32 = null;
|
||||||
|
print("a={}\n", unwrap_or(99, bi));
|
||||||
|
ci : ?i32 = 5;
|
||||||
|
print("b={}\n", unwrap_or(99, ci));
|
||||||
|
|
||||||
|
// ?i64 instantiation.
|
||||||
|
bl : ?i64 = null;
|
||||||
|
print("c={}\n", unwrap_or(7, bl));
|
||||||
|
dl : ?i64 = 42;
|
||||||
|
print("d={}\n", unwrap_or(7, dl));
|
||||||
|
|
||||||
|
// ?*i64 instantiation: null -> the &v default.
|
||||||
|
bp : ?*i64 = null;
|
||||||
|
v : i64 = 123;
|
||||||
|
print("e={}\n", unwrap_or(@v, bp).*);
|
||||||
|
|
||||||
|
// ?Struct instantiation: null -> default struct, present -> the value.
|
||||||
|
ps : ?Pt = null;
|
||||||
|
rp := unwrap_or(Pt.{ x = 1, y = 2 }, ps);
|
||||||
|
print("f={} {}\n", rp.x, rp.y);
|
||||||
|
qs : ?Pt = Pt.{ x = 8, y = 9 };
|
||||||
|
rq := unwrap_or(Pt.{ x = 1, y = 2 }, qs);
|
||||||
|
print("g={} {}\n", rq.x, rq.y);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// `??` on an alias-of-optional with a struct-literal default — present + null.
|
||||||
|
//
|
||||||
|
// Regression (issue 0180): `?Opt ?? S.{...}` where `Opt :: ?S` must resolve
|
||||||
|
// the alias child to the same struct TypeId on both branches and lower the
|
||||||
|
// struct-literal default against that resolved child (issue-0166 threading),
|
||||||
|
// so the present-payload and the default share the merge TypeId. Both the
|
||||||
|
// null path (default) and the present path (the payload) are exercised.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
S :: struct { a: i64 = 0; b: i64 = 0; }
|
||||||
|
Opt :: ?S;
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
// null lhs -> the struct-literal default.
|
||||||
|
o : Opt = null;
|
||||||
|
x := o ?? S.{ a = 7, b = 8 };
|
||||||
|
print("a={} {}\n", x.a, x.b);
|
||||||
|
|
||||||
|
// present lhs -> the payload, default ignored.
|
||||||
|
o2 : Opt = S.{ a = 1, b = 2 };
|
||||||
|
y := o2 ?? S.{ a = 7, b = 8 };
|
||||||
|
print("b={} {}\n", y.a, y.b);
|
||||||
|
|
||||||
|
// untyped `.{}` default against the alias child (issue-0166 path).
|
||||||
|
o3 : Opt = null;
|
||||||
|
z := o3 ?? .{};
|
||||||
|
print("c={} {}\n", z.a, z.b);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Annotated local var-decl `x : ?B = <a ?A value>` preserves the SOURCE
|
||||||
|
// optional's presence across a payload coercion.
|
||||||
|
//
|
||||||
|
// Regression (issue 0180): `lowerVarDecl`'s optional-target branch hand-rolled
|
||||||
|
// the optional handling — for a non-null initializer it coerced to the
|
||||||
|
// optional's CHILD (classifying `?A → child_B` as an unconditional unwrap → 0
|
||||||
|
// for a null source) then re-wrapped with a hardcoded present has-bit. So a
|
||||||
|
// null `?A` source became a PRESENT `?B` carrying zero. The fix routes an
|
||||||
|
// optional source through the presence-preserving `.optional_to_optional`
|
||||||
|
// coercion (the general trailing coerce) instead of the wrap-present path,
|
||||||
|
// which now only handles a genuine non-optional `T → ?T` wrap.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Shape :: protocol { area :: (self: *Self) -> i64; }
|
||||||
|
Sq :: struct { s: i64; }
|
||||||
|
impl Shape for Sq { area :: (self: *Sq) -> i64 { return self.s * self.s; } }
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
// Widen ?i32 → ?i64: null stays absent, present carries its value.
|
||||||
|
a : ?i32 = null;
|
||||||
|
b : ?i64 = a;
|
||||||
|
if b { print("widen-null: present\n"); } else { print("widen-null: absent\n"); }
|
||||||
|
c : ?i32 = 7;
|
||||||
|
d : ?i64 = c;
|
||||||
|
print("widen-pres: {}\n", d ?? -1);
|
||||||
|
|
||||||
|
// Narrow ?i64 → ?i32.
|
||||||
|
e : ?i64 = null;
|
||||||
|
f : ?i32 = e;
|
||||||
|
if f { print("narrow-null: present\n"); } else { print("narrow-null: absent\n"); }
|
||||||
|
g : ?i64 = 9;
|
||||||
|
h : ?i32 = g;
|
||||||
|
print("narrow-pres: {}\n", h ?? -1);
|
||||||
|
|
||||||
|
// int → float ?i32 → ?f64.
|
||||||
|
i : ?i32 = null;
|
||||||
|
j : ?f64 = i;
|
||||||
|
if j { print("i2f-null: present\n"); } else { print("i2f-null: absent\n"); }
|
||||||
|
|
||||||
|
// A genuine non-optional wrap `T → ?T` stays present.
|
||||||
|
k : ?i64 = 5;
|
||||||
|
print("wrap: {}\n", k ?? -1);
|
||||||
|
|
||||||
|
// Aggregate payload: ?[3]i64 → ?[]i64 (presence-preserving array→slice).
|
||||||
|
arr : ?[3]i64 = .[10, 20, 30];
|
||||||
|
sl : ?[]i64 = arr;
|
||||||
|
if s := sl { print("arr2slice-pres: len={} first={}\n", s.len, s[0]); }
|
||||||
|
narr : ?[3]i64 = null;
|
||||||
|
nsl : ?[]i64 = narr;
|
||||||
|
if nsl { print("arr2slice-null: present\n"); } else { print("arr2slice-null: absent\n"); }
|
||||||
|
|
||||||
|
// Aggregate payload: ?Sq → ?Shape (presence-preserving protocol erasure).
|
||||||
|
sq : ?Sq = Sq.{ s = 4 };
|
||||||
|
sh : ?Shape = sq;
|
||||||
|
if x := sh { print("erase-pres: area={}\n", x.area()); }
|
||||||
|
nsq : ?Sq = null;
|
||||||
|
nsh : ?Shape = nsq;
|
||||||
|
if nsh { print("erase-null: present\n"); } else { print("erase-null: absent\n"); }
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
a=99
|
||||||
|
b=5
|
||||||
|
c=7
|
||||||
|
d=42
|
||||||
|
e=123
|
||||||
|
f=1 2
|
||||||
|
g=8 9
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
a=7 8
|
||||||
|
b=1 2
|
||||||
|
c=0 0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
widen-null: absent
|
||||||
|
widen-pres: 7
|
||||||
|
narrow-null: absent
|
||||||
|
narrow-pres: 9
|
||||||
|
i2f-null: absent
|
||||||
|
wrap: 5
|
||||||
|
arr2slice-pres: len=3 first=10
|
||||||
|
arr2slice-null: absent
|
||||||
|
erase-pres: area=16
|
||||||
|
erase-null: absent
|
||||||
@@ -1,5 +1,28 @@
|
|||||||
# 0180 — `??` lowering defects for generic / alias / tuple optional lhs (wrong fallback, segfault, PHI mismatch)
|
# 0180 — `??` lowering defects for generic / alias / tuple optional lhs (wrong fallback, segfault, PHI mismatch)
|
||||||
|
|
||||||
|
> **RESOLVED.** (1) The generic-`??`-wrong-fallback was NOT in `lowerNullCoalesce`
|
||||||
|
> — the real root cause was that an optional→optional coercion `?A → ?B` (differing
|
||||||
|
> payload, e.g. the `?i32 → ?i64` call-arg coercion when instantiating
|
||||||
|
> `unwrap_or(99, ?i32)`) routed through `.optional_wrap`, which unconditionally
|
||||||
|
> UNWRAPPED the source and re-wrapped as ALWAYS-PRESENT — so a NULL became a
|
||||||
|
> present-zero everywhere (call args, returns, field init, var-decl, `??`). Fix:
|
||||||
|
> a `CoercionPlan.optional_to_optional` (`src/ir/conversions.zig`) + a
|
||||||
|
> presence-preserving arm in `coerceMode` (`src/ir/lower/coerce.zig`): has_value →
|
||||||
|
> present: unwrap+coerce-child+wrap-present; absent: `constNull(dst)`; merge via a
|
||||||
|
> `dst_ty` block param. `lowerVarDecl` (`src/ir/lower/stmt.zig`) gained a
|
||||||
|
> `!src_is_optional` guard so an annotated `x : ?B = <?A>` routes through the same
|
||||||
|
> arm (also makes aggregate-payload var-decl `?[3]i64 → ?[]i64` / `?Concrete →
|
||||||
|
> ?Protocol` work). (2) The alias-optional struct-literal default already works
|
||||||
|
> (grouping + issue-0166 threading) — locked by regression. (3) `?(i32)` is now a
|
||||||
|
> grouped `?i32` (issue-0177 grouping); a genuine 1-tuple default `?(i32,) ?? 5`
|
||||||
|
> emits a clean diagnostic (`lowerNullCoalesce`, `src/ir/lower/expr.zig`) instead
|
||||||
|
> of an LLVM PHI-verifier abort (no implicit scalar→1-tuple coercion per spec).
|
||||||
|
> Regressions: `examples/optionals/0916` (generic ??), `0917` (alias struct
|
||||||
|
> default), `0918` (var-decl optional→optional present/null × widen/narrow/
|
||||||
|
> int-float/array→slice/erasure), `examples/diagnostics/1202` (1-tuple-default
|
||||||
|
> diagnostic) + a `conversions.test.zig` unit test. Verified by 3 adversarial
|
||||||
|
> reviews; suite 798/0.
|
||||||
|
|
||||||
## Symptom
|
## Symptom
|
||||||
|
|
||||||
`lowerNullCoalesce` mishandles several non-trivial optional lhs shapes (all with a
|
`lowerNullCoalesce` mishandles several non-trivial optional lhs shapes (all with a
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ test "conversions: classify covers the built-in coercion ladder" {
|
|||||||
try std.testing.expectEqual(Plan.optional_unwrap, cr.classify(opt_i64, .i64));
|
try std.testing.expectEqual(Plan.optional_unwrap, cr.classify(opt_i64, .i64));
|
||||||
try std.testing.expectEqual(Plan.void_to_optional, cr.classify(.void, opt_i64));
|
try std.testing.expectEqual(Plan.void_to_optional, cr.classify(.void, opt_i64));
|
||||||
|
|
||||||
|
// `?A → ?B` (differing payloads) is a presence-preserving payload coercion,
|
||||||
|
// NOT the always-present unwrap-then-rewrap that the `.optional_wrap` arm
|
||||||
|
// produced (issue 0180). Same-payload optionals are `.no_op`.
|
||||||
|
const opt_i32 = tt.optionalOf(.i32);
|
||||||
|
try std.testing.expectEqual(Plan.optional_to_optional, cr.classify(opt_i32, opt_i64));
|
||||||
|
try std.testing.expectEqual(Plan.no_op, cr.classify(opt_i64, opt_i64));
|
||||||
|
|
||||||
// `?T → bool` is NOT an unwrap-then-narrow presence test (issue 0169):
|
// `?T → bool` is NOT an unwrap-then-narrow presence test (issue 0169):
|
||||||
// it must reject, never silently produce `false`. But `?bool → bool`
|
// it must reject, never silently produce `false`. But `?bool → bool`
|
||||||
// is a genuine unwrap of a bool payload.
|
// is a genuine unwrap of a bool payload.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub const CoercionResolver = struct {
|
|||||||
optional_unwrap, // ?T → concrete (narrowing)
|
optional_unwrap, // ?T → concrete (narrowing)
|
||||||
optional_to_bool_reject, // ?T → bool (no presence-test coercion; diagnostic)
|
optional_to_bool_reject, // ?T → bool (no presence-test coercion; diagnostic)
|
||||||
void_to_optional, // void (null literal) → ?T
|
void_to_optional, // void (null literal) → ?T
|
||||||
|
optional_to_optional, // ?A → ?B (presence-preserving payload coercion)
|
||||||
optional_wrap, // concrete → ?T
|
optional_wrap, // concrete → ?T
|
||||||
erase_protocol, // concrete → protocol value
|
erase_protocol, // concrete → protocol value
|
||||||
int_to_float,
|
int_to_float,
|
||||||
@@ -114,6 +115,16 @@ pub const CoercionResolver = struct {
|
|||||||
if (child_ty == dst_ty or (dst_ty.isBuiltin() and child_ty.isBuiltin())) {
|
if (child_ty == dst_ty or (dst_ty.isBuiltin() and child_ty.isBuiltin())) {
|
||||||
return .optional_unwrap;
|
return .optional_unwrap;
|
||||||
}
|
}
|
||||||
|
// ?A → ?B: a presence-preserving payload coercion. Without a
|
||||||
|
// dedicated arm this fell to `.optional_wrap` (dst is optional),
|
||||||
|
// which unwrapped the SOURCE optional unconditionally and re-
|
||||||
|
// wrapped it as always-present — turning a null `?i32` into a
|
||||||
|
// present `?i64` carrying the zero payload (issue 0180: generic
|
||||||
|
// `??` returning the wrong fallback). Only meaningful when the
|
||||||
|
// children differ (same-type optionals are `.no_op` already).
|
||||||
|
if (!dst_ty.isBuiltin() and self.l.module.types.get(dst_ty) == .optional) {
|
||||||
|
return .optional_to_optional;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -683,6 +683,39 @@ pub fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mod
|
|||||||
},
|
},
|
||||||
// void → Optional: produce null (void is the type of null_literal)
|
// void → Optional: produce null (void is the type of null_literal)
|
||||||
.void_to_optional => return self.builder.constNull(dst_ty),
|
.void_to_optional => return self.builder.constNull(dst_ty),
|
||||||
|
// ?A → ?B: presence-preserving payload coercion. The naive route
|
||||||
|
// (fall through to `.optional_wrap`) unwrapped the SOURCE optional
|
||||||
|
// unconditionally and re-wrapped as always-present, dropping the
|
||||||
|
// has-bit — a null `?i32` became a present `?i64` carrying zero
|
||||||
|
// (issue 0180: generic `??` returning the wrong fallback). Branch on
|
||||||
|
// the source's presence so the payload is only unwrapped when it is
|
||||||
|
// actually present (the interp errors on unwrapping a null optional,
|
||||||
|
// so a branchless select is not an option):
|
||||||
|
// present → unwrap src child, coerce to dst child, wrap as present
|
||||||
|
// absent → null of the destination optional
|
||||||
|
.optional_to_optional => {
|
||||||
|
const src_child = self.module.types.get(src_ty).optional.child;
|
||||||
|
const dst_child = self.module.types.get(dst_ty).optional.child;
|
||||||
|
|
||||||
|
const has_val = self.builder.optionalHasValue(val);
|
||||||
|
const present_bb = self.freshBlock("opt2opt.present");
|
||||||
|
const absent_bb = self.freshBlock("opt2opt.absent");
|
||||||
|
const merge_bb = self.freshBlockWithParams("opt2opt.merge", &.{dst_ty});
|
||||||
|
self.builder.condBr(has_val, present_bb, &.{}, absent_bb, &.{});
|
||||||
|
|
||||||
|
self.builder.switchToBlock(present_bb);
|
||||||
|
const unwrapped = self.builder.optionalUnwrap(val, src_child);
|
||||||
|
const coerced_child = self.coerceMode(unwrapped, src_child, dst_child, mode);
|
||||||
|
const wrapped = self.builder.optionalWrap(coerced_child, dst_ty);
|
||||||
|
self.builder.br(merge_bb, &.{wrapped});
|
||||||
|
|
||||||
|
self.builder.switchToBlock(absent_bb);
|
||||||
|
const null_dst = self.builder.constNull(dst_ty);
|
||||||
|
self.builder.br(merge_bb, &.{null_dst});
|
||||||
|
|
||||||
|
self.builder.switchToBlock(merge_bb);
|
||||||
|
return self.builder.blockParam(merge_bb, 0, dst_ty);
|
||||||
|
},
|
||||||
// Concrete → Optional wrapping (coerce to the inner type first)
|
// Concrete → Optional wrapping (coerce to the inner type first)
|
||||||
.optional_wrap => {
|
.optional_wrap => {
|
||||||
const child_ty = self.module.types.get(dst_ty).optional.child;
|
const child_ty = self.module.types.get(dst_ty).optional.child;
|
||||||
|
|||||||
@@ -2065,6 +2065,25 @@ pub fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
// The merge-block param, the unwrapped LHS, and the coerced RHS must all
|
||||||
|
// share `inner_ty` — they feed the same PHI. If the RHS default still does
|
||||||
|
// not match after coercion (e.g. a scalar `5` default against an optional
|
||||||
|
// whose payload is a 1-tuple `(i32,)`: there is no implicit scalar→1-tuple
|
||||||
|
// coercion — a 1-tuple value is written `(5,)`), branching with the
|
||||||
|
// mismatched type emits a `phi {i32}` vs `i32` that aborts the LLVM
|
||||||
|
// verifier (issue 0180). Diagnose loudly and br with a typed placeholder so
|
||||||
|
// the PHI stays well-formed; `hasErrors()` aborts before codegen anyway.
|
||||||
|
const coerced_ty = self.builder.getRefType(rhs);
|
||||||
|
if (coerced_ty != inner_ty and coerced_ty != .void and inner_ty != .void) {
|
||||||
|
if (self.diagnostics) |d| {
|
||||||
|
const note: []const u8 = if (!inner_ty.isBuiltin() and self.module.types.get(inner_ty) == .tuple)
|
||||||
|
" (note: a 1-tuple value is written '(x,)' with a trailing comma)"
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
d.addFmt(.err, nc.rhs.span, "'??' default has type '{s}', but the optional's payload is '{s}'{s}", .{ self.formatTypeName(coerced_ty), self.formatTypeName(inner_ty), note });
|
||||||
|
}
|
||||||
|
rhs = self.builder.constNull(inner_ty); // typed placeholder — keeps the PHI well-formed
|
||||||
|
}
|
||||||
self.builder.br(merge_bb, &.{rhs});
|
self.builder.br(merge_bb, &.{rhs});
|
||||||
|
|
||||||
// Continue at merge
|
// Continue at merge
|
||||||
|
|||||||
@@ -314,7 +314,18 @@ pub fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void {
|
|||||||
// fail LLVM verification (issue 0160).
|
// fail LLVM verification (issue 0160).
|
||||||
if (!ty.isBuiltin()) {
|
if (!ty.isBuiltin()) {
|
||||||
const ty_info = self.module.types.get(ty);
|
const ty_info = self.module.types.get(ty);
|
||||||
if (ty_info == .optional and val.data != .null_literal and self.builder.getRefType(ref) != ty) {
|
// Is the initializer value ITSELF an optional (`?A`)? If so the
|
||||||
|
// target `?B` is a presence-PRESERVING coercion (`.optional_to_optional`),
|
||||||
|
// NOT a wrap-present. The manual unwrap-to-child + `optionalWrap(present)`
|
||||||
|
// below classifies `?A → child_B` as an unconditional unwrap (→ 0 for a
|
||||||
|
// null source) then wraps it as always-present, so a null `?A` becomes a
|
||||||
|
// present `?B` carrying zero (issue 0180). Route an optional source through
|
||||||
|
// the trailing general `coerceToType(?A → ?B)` instead, which dispatches to
|
||||||
|
// the presence-preserving arm. The wrap-present path below stays correct for
|
||||||
|
// a non-optional source `T → ?T`.
|
||||||
|
const ref_ty0 = self.builder.getRefType(ref);
|
||||||
|
const src_is_optional = !ref_ty0.isBuiltin() and self.module.types.get(ref_ty0) == .optional;
|
||||||
|
if (ty_info == .optional and val.data != .null_literal and ref_ty0 != ty and !src_is_optional) {
|
||||||
// Coerce to the optional's CHILD first (e.g. an array value
|
// Coerce to the optional's CHILD first (e.g. an array value
|
||||||
// into a `?[]T` promotes array→slice), THEN wrap — wrapping
|
// into a `?[]T` promotes array→slice), THEN wrap — wrapping
|
||||||
// the raw value would store e.g. array bits into the slice
|
// the raw value would store e.g. array bits into the slice
|
||||||
|
|||||||
Reference in New Issue
Block a user