fix: diagnose ?(?T) tuple-payload mismatch instead of malformed IR (issue 0165)
In type position (T) is a 1-tuple (specs.md:843), so ?(?i64) is optional(tuple(?i64)); assigning a bare ?i64 had coerceToType classify .none and pass the value through, then optionalWrap built a corrupt insertvalue that aborted the LLVM verifier. After coercing toward an optional's child, verify the coerced type equals the child type (stmt.zig decl-init + coerce.zig .optional_wrap); on mismatch emit a located diagnostic (tuple-specific note only when the child is a tuple). formatTypeName now renders tuples as (x: i64, y: i64). Regressions: optionals/0911 (nested optional via alias, round-trip), diagnostics/1195 (the mismatch diagnostic). Updated diagnostics/1101 + protocols/0414 goldens for the improved tuple type-name rendering. Verified by 3 adversarial reviews. Filed adjacent bug 0171 (?any child not canonicalized).
This commit is contained in:
@@ -1,5 +1,22 @@
|
||||
# 0165 — parenthesized nested optional `?(?T)` resolves to a malformed double-wrapped type
|
||||
|
||||
> **RESOLVED.** The issue's premise was partly wrong: per `specs.md:843`, in
|
||||
> TYPE position `(T)` is a single-field TUPLE, not a grouping — so `?(?i64)` is
|
||||
> `optional(tuple(?i64))` and the compiler's `{ {{i64,i1}}, i1 }` layout was
|
||||
> CORRECT. The real bug was a silent malformed-IR path: assigning a bare `?i64`
|
||||
> to it had `coerceToType` classify `.none` and pass the value through unchanged,
|
||||
> then `optionalWrap` built a corrupt `insertvalue` that aborted the LLVM
|
||||
> verifier. Fix: after coercing toward an optional's child, verify the coerced
|
||||
> value's type equals the child type (`src/ir/lower/stmt.zig` decl-init +
|
||||
> `src/ir/lower/coerce.zig` `.optional_wrap`); on mismatch emit a located
|
||||
> diagnostic (with a tuple-specific note only when the child is a tuple) instead
|
||||
> of corrupt IR. `formatTypeName` now renders tuples as `(x: i64, y: i64)`.
|
||||
> Genuine nested optionals via alias (`Opt :: ?i64; ?Opt`) work and round-trip.
|
||||
> Regressions: `examples/optionals/0911-nested-optional-via-alias.sx`,
|
||||
> `examples/diagnostics/1195-diagnostics-err-parenthesized-optional-tuple.sx`.
|
||||
> Verified by 3 adversarial reviews. (Adjacent pre-existing bug found + filed:
|
||||
> 0171 `?any` child not canonicalized.)
|
||||
|
||||
## Symptom
|
||||
|
||||
A nested optional written `?(?i64)` resolves to a spurious extra struct wrapper:
|
||||
|
||||
47
issues/0171-optional-any-child-not-canonicalized.md
Normal file
47
issues/0171-optional-any-child-not-canonicalized.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 0171 — `?any` optional child is a non-canonical `any` TypeId (box-into-any rule misses, value silently discarded)
|
||||
|
||||
## Symptom
|
||||
|
||||
An optional whose child is `any` (`?any`) is broken. Baseline (before the issue
|
||||
0165 fix) silently DISCARDED the boxed value: `x : ?any = 42; v := x!` yields an
|
||||
empty box `any{}`, not `42` — the payload is lowered as a zero-size `{}`. After
|
||||
the 0165 fix the same code now produces a clean type-mismatch diagnostic
|
||||
(`cannot assign a value of type 'i64' to optional '?any': its payload type is
|
||||
'any'`), which is strictly better than silent corruption but still means `?any`
|
||||
does not work.
|
||||
|
||||
## Root cause (from adversarial review of issue 0165)
|
||||
|
||||
The box-into-`any` coercion rule (`src/ir/conversions.zig` ~line 57) keys on the
|
||||
BUILTIN `.any` enum TypeId. But an optional's child `any` is a SEPARATELY
|
||||
interned TypeId (observed `@enumFromInt(246)`, type-name `"any"`) that is NOT
|
||||
identity-equal to the builtin `.any`. So `classify(i64, child_any)` falls through
|
||||
to `.none`, returns the value unchanged (`i64`), and the wrap is invalid. The
|
||||
`any` type is not being canonicalized to the builtin TypeId when it appears as an
|
||||
optional child.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
main :: () {
|
||||
x : ?any = 42;
|
||||
v := x!;
|
||||
print("{}\n", v); // expected: a boxed 42; baseline yields empty any{}
|
||||
}
|
||||
```
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
Canonicalize `any` as an optional child (and likely any other compound position)
|
||||
to the builtin `.any` TypeId at type-resolution/interning time, so the
|
||||
box-into-any rule in `src/ir/conversions.zig` classifies correctly and `?any`
|
||||
round-trips. Find where the optional child type is resolved/interned
|
||||
(`src/ir/types.zig` `optionalOf` / the type resolver) and ensure an `any` child
|
||||
maps to the canonical builtin TypeId rather than a fresh interned copy.
|
||||
Alternatively, make the box-into-any classifier compare by type-KIND
|
||||
(`info == .any`) rather than TypeId identity — but canonicalization is the more
|
||||
robust fix (it also fixes `==`, `size_of`, and any other identity check on the
|
||||
`any` child). Verify the repro round-trips a boxed value; add an
|
||||
`examples/types/01xx-optional-any.sx` regression. Low priority — `?any` is used
|
||||
nowhere in `library/` or `examples/`.
|
||||
Reference in New Issue
Block a user