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

@@ -1,5 +1,28 @@
# 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
`lowerNullCoalesce` mishandles several non-trivial optional lhs shapes (all with a