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
|
||||
Reference in New Issue
Block a user