Files
sx/issues/0171-optional-any-child-not-canonicalized.md
agra 0bc8005b99 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).
2026-06-22 21:54:12 +03:00

2.2 KiB

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

#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/.