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:
agra
2026-06-23 16:16:47 +03:00
parent 4ca466fa96
commit 097d23d909
22 changed files with 279 additions and 1 deletions

View File

@@ -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.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):
// it must reject, never silently produce `false`. But `?bool → bool`
// is a genuine unwrap of a bool payload.