fix: struct-literal → optional coercion + #get through optional chain (issue 0160)

Two fixes for optional interactions surfaced by the #set/#get review. The
original issue 0160 mis-diagnosed (A) as an optional-chain bug; the chain works
fine for real fields. The actual bugs:

(A) A bare struct literal `.{ ... }` against an optional target `?T` was built
into the optional's {payload, has_value} layout instead of the inner T, then
re-wrapped — corrupting the value (a multi-field payload's first field clobbered
by the has_value flag, or a `?T` arg silently null) or failing LLVM
verification. lowerStructLiteral now builds the inner T, materializes it, and
wraps via coerceToType; lowerVarDecl's previously-UNCONDITIONAL optional wrap is
guarded so an already-`?T` value isn't double-wrapped. Fixed across var-decl,
arg, return, nested field, reassignment, and array-element contexts.

(B) `#get` accessors are now reachable through an optional chain (`obj?.getter`):
lowerOptionalChain dispatches the getter via a synthetic receiver, and
expr_typer types `obj?.getter` through a shared getterReturnTypeOnDeref helper
(handles `?T` and `?*T`, value and pointer optionals, and generic-instance
getters like List.len). The `#set` write side through `?.` is intentionally left
matching real-field behavior (optional-chain assignment unsupported).

Regression tests: examples/optionals/0906 (struct-literal → optional) and 0907
(accessor through chain). issues/0160 marked RESOLVED with the corrected root
cause.
This commit is contained in:
agra
2026-06-22 18:28:57 +03:00
parent 9523c29173
commit 1b0c857b91
13 changed files with 232 additions and 4 deletions

View File

@@ -1,5 +1,23 @@
# 0160 — optional-chain field access: `?T` value-optional read miscompiles, and `#get`/`#set` accessors aren't reached through `?.`
> **RESOLVED.** Root cause was MIS-DIAGNOSED in the original writeup below: (A)
> was NOT an optional-chain bug — the chain works fine for real fields. The real
> bug was **struct-literal → optional coercion**: a bare `.{ ... }` against a
> `?T` target was built into the optional's `{payload, has_value}` layout
> instead of building the inner `T` and wrapping it. Fix:
> `lowerStructLiteral` (`src/ir/lower/expr.zig`) now builds the inner `T`,
> materializes it, and wraps via `coerceToType`; and `lowerVarDecl`
> (`src/ir/lower/stmt.zig`) only wraps when the value is not already the target
> optional (its previous UNCONDITIONAL wrap double-wrapped any already-`?T`
> value). (B) the `#get`-through-`?.` gap was real and is fixed:
> `lowerOptionalChain` dispatches the getter via a synthetic receiver, and
> `expr_typer` types `obj?.getter` through a shared `getterReturnTypeOnDeref`
> helper (handles `?T` and `?*T`). The `#set` write side through `?.` is
> intentionally left matching real-field behavior (optional-chain assignment is
> unsupported for all fields) and was never part of this bug. Regression tests:
> `examples/optionals/0906-optionals-struct-literal-into-optional.sx` and
> `examples/optionals/0907-optionals-accessor-through-chain.sx`.
## Symptom
Two related gaps in optional-chain field access (`obj?.field`), surfaced while