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:
@@ -36,6 +36,7 @@ pub const CoercionResolver = struct {
|
||||
optional_unwrap, // ?T → concrete (narrowing)
|
||||
optional_to_bool_reject, // ?T → bool (no presence-test coercion; diagnostic)
|
||||
void_to_optional, // void (null literal) → ?T
|
||||
optional_to_optional, // ?A → ?B (presence-preserving payload coercion)
|
||||
optional_wrap, // concrete → ?T
|
||||
erase_protocol, // concrete → protocol value
|
||||
int_to_float,
|
||||
@@ -114,6 +115,16 @@ pub const CoercionResolver = struct {
|
||||
if (child_ty == dst_ty or (dst_ty.isBuiltin() and child_ty.isBuiltin())) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user