Files
sx/issues/0165-parenthesized-nested-optional-malformed.md
agra 555ccdc024 feat: parenthesized type grouping — (T) groups, (T,) is a 1-tuple (issue 0177)
In type position, parentheses now mirror value position: (T) (a single
unnamed element, no trailing comma) is a GROUPING that resolves to the
inner type; (T,) is a 1-tuple; (A, B) a 2-tuple; named (x: T) and spread
(..Ts) stay tuples; (...) -> R stays a function type. This lets a
closure/optional/function type be parenthesized for readability without
silently becoming a 1-tuple:
  [1](Closure(i64,i64) -> i64)   // array of closures (issue 0177) -> 7
  ?(?i64)                        // genuine nested optional (issue 0165 intent)

Parser: src/parser.zig returns the inner node for a single unnamed
non-spread no-trailing-comma parenthesized type. formatTypeName (both
generic.zig diagnostics + types.zig reflection) now render a 1-tuple as
(T,) so the spelling is unambiguous and diagnostics are self-consistent.
The 0165 coerce/stmt note reworded accordingly.

specs.md §Type Syntax updated; basic/0036 wrap return -> (i64,); obsolete
diagnostic 1195 removed (?(?i64) now compiles); regression
examples/types/0201-types-parenthesized-type-grouping.sx added; 0414 .ir
golden regenerated for the (T,) rendering. Resolves 0177; updates
0165/0170. Verified by 3 adversarial reviews; suite 792/0.
2026-06-23 10:43:47 +03:00

74 lines
3.7 KiB
Markdown

# 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`.
> **UPDATE (grouping):** `(T)` in type position is now a GROUPING, not a
> 1-tuple, so `?(?i64)` is a genuine NESTED OPTIONAL and compiles (what this
> issue originally wanted). The coerce guard now only fires for an explicit
> 1-tuple child `?(T,)` mismatch (note reworded). The obsolete diagnostic
> example 1195 was removed; see `examples/types/0201-types-parenthesized-type-grouping.sx`.
> Verified by 3 adversarial reviews. (A review noted `?any` over-rejection;
> that turned out to be a casing non-bug — lowercase `any` is an undefined name,
> the type is `Any`; see 0171, closed NOT-A-BUG.)
## Symptom
A nested optional written `?(?i64)` resolves to a spurious extra struct wrapper:
the destination type lowers to `{ { {i64,i1} }, i1 }` (triple-wrapped) instead of
the correct `{ {i64,i1}, i1 }`. Assigning an inner `?i64` then fails the LLVM
verifier:
```
LLVM verification failed: Invalid InsertValueInst operands!
%ow.val = insertvalue { { { i64, i1 } }, i1 } undef, { i64, i1 } %load, 0
```
Crash (non-zero exit). `??i64` (unparenthesized) is a separate parse error
(`expected type name`) and is NOT this bug — only the parenthesized `?(?T)` form
reaches type resolution and produces the malformed layout.
## Reproduction
```sx
#import "modules/std.sx";
main :: () {
inner : ?i64 = 5;
outer : ?(?i64) = inner;
print("ok\n");
}
```
Expected: `ok` (a well-formed `{ {i64,i1}, i1 }` outer optional wrapping the
inner `?i64`). Observed: LLVM verifier abort.
## Investigation prompt
The type-table interning (`src/ir/types.zig` `optionalOf` / the optional
lowering near `types.zig:87`) produces the CORRECT `{ {i64,i1}, i1 }` for a
real `optional(optional(i64))`. So the malformed layout comes from
`instruction.ty` itself: the parenthesized `(?i64)` inner type expression is
resolved as a single-field STRUCT wrapping `?i64`, not as the optional type
directly. Suspected area: resolution of a parenthesized type expression
(`src/ir/type_resolver.zig` and/or the parser's handling of `(?T)` as a type) —
a parenthesized type should resolve to the inner type unchanged, not introduce a
tuple/struct wrapper. `src/backend/llvm/ops.zig` `emitOptionalWrap` is the
faithful victim (it uses `toLLVMType(instruction.ty)`), not the cause.
Verify: the repro prints `ok`; `?(?i64)` round-trips (unwrap inner, read value);
confirm the IR type is `{ {i64,i1}, i1 }`. Add an
`examples/optionals/09xx-nested-parenthesized-optional.sx` regression. (The
`unresolved type` panic seen when an `!` unwrap is added is downstream recovery
fallout of the same root cause — should disappear once the layout is correct.)