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:
@@ -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) {
|
||||
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});
|
||||
|
||||
// Continue at merge
|
||||
|
||||
Reference in New Issue
Block a user