feat: #set property accessors (write counterpart of #get)
A method `name :: (self: *T, value: V) #set { ... }` (or `=> expr;`) is the
write counterpart of a `#get` accessor: `obj.name = rhs` dispatches to it as
`obj.name(rhs)` when no real field matches. Plumbed parallel to `#get`:
- lexer/token `#set`; `FnDecl.is_set` + `Function.is_set`; parsed in the same
marker slot as `#get` (no return type, exactly self + one value param).
- get+set coexistence: a setter registers/mangles/dispatches under an effective
`name$set` name (`$` is illegal in sx identifiers, so unmistakable), keeping a
same-name `#get` under the plain `name`. Resolution is declaration-order-
independent: a plain read query picks the non-setter, a `name$set` write query
picks the setter (accessorEffName / accessorNameMatches / structMethodFn).
- write dispatch in lowerAssignment via tryLowerPropertyAssignment: plain assign
synthesizes `obj.name$set(rhs)`; compound `OP=` is get-modify-set and
evaluates the receiver EXACTLY ONCE (bound to a synthetic local); read-only
(#get-only) and write-only (#set-only + compound) emit clear diagnostics; a
real field of the same name still wins. Multi-assign property targets dispatch
the setter too (tryLowerPropertyStore, via a pre-lowered-Ref binding).
Payoff: List gains a `len` #set, so `xs.len = n` works; the `.items.len = N`
write workarounds in sched.sx + ui/* + platform/* revert to `xs.len = N`.
issues/0160 records an optional-chain interaction surfaced by the review (a
pre-existing `?T` value-optional read miscompile that blocks getter-through-`?.`).
This commit is contained in:
92
issues/0160-optional-chain-value-optional-and-accessors.md
Normal file
92
issues/0160-optional-chain-value-optional-and-accessors.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# 0160 — optional-chain field access: `?T` value-optional read miscompiles, and `#get`/`#set` accessors aren't reached through `?.`
|
||||
|
||||
## Symptom
|
||||
|
||||
Two related gaps in optional-chain field access (`obj?.field`), surfaced while
|
||||
extending property accessors (`#get`/`#set`) — but the root problem (A) is
|
||||
PRE-EXISTING and independent of accessors:
|
||||
|
||||
- **(A) `?T` value-optional read of a real field miscompiles.** `ot?.raw` where
|
||||
`ot : ?T` (optional of a *value* struct) fails LLVM verification:
|
||||
`Invalid InsertValueInst operands! ... insertvalue { { i64 }, i1 } undef, { { i64 }, i1 } %si, 0`
|
||||
— the some-branch builds the result optional by inserting the WHOLE
|
||||
`{payload, has_value}` aggregate where the bare payload is expected.
|
||||
Observed: LLVM verification failure (compile abort). Expected: prints `7`.
|
||||
*(The `?*T` pointer-optional form of the same read works correctly, so the
|
||||
bug is specific to value-optionals.)*
|
||||
|
||||
- **(B) `#get`/`#set` accessors are not reached through `?.`.** `pt?.p` where
|
||||
`p` is a `#get` accessor gives `field 'p' not found on type '*T'` — the
|
||||
optional-chain read path (`lowerOptionalChain`) resolves only real fields, not
|
||||
accessors. (The write form `obj?.p = x` is consistent with real fields, which
|
||||
also reject optional-chain assignment, so the write side is NOT part of this
|
||||
issue.)
|
||||
|
||||
(B) is blocked on (A): a correct `obj?.getter` read must run the getter inside
|
||||
the optional's some-branch and re-wrap the result as `?R`, i.e. it reuses the
|
||||
exact some-branch/merge optional-construction path that (A) miscompiles for
|
||||
value-optionals. Layering accessor dispatch onto that path while it miscompiles
|
||||
would bake the same bug into accessors.
|
||||
|
||||
## Reproduction
|
||||
|
||||
(A) — value-optional real-field read (LLVM verify failure):
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
T :: struct { raw: i64 = 7; }
|
||||
main :: () {
|
||||
ot : ?T = .{ raw = 7 };
|
||||
print("{}\n", ot?.raw); // expected: 7 — actual: LLVM verification failure
|
||||
}
|
||||
```
|
||||
|
||||
(B) — accessor through optional chain (field-not-found):
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
T :: struct {
|
||||
raw: i64 = 0;
|
||||
p :: (self: *T) -> i64 #get => self.raw;
|
||||
}
|
||||
main :: () {
|
||||
t : T = .{ raw = 4 };
|
||||
pt : ?*T = @t;
|
||||
print("{}\n", pt?.p); // expected: 4 — actual: field 'p' not found on type '*T'
|
||||
}
|
||||
```
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
Fix (A) first; (B) builds on it.
|
||||
|
||||
**(A)** In `src/ir/lower/expr.zig` `lowerOptionalChain` (the some-branch around
|
||||
the `optional_wrap` of `field_val`): when the optional's child is a *value*
|
||||
struct (`?T`, not `?*T`), the result optional is mis-assembled — the verifier
|
||||
sees a `{ {i64}, i1 }` inserted into slot 0 of `{ {i64}, i1 }` instead of the
|
||||
bare `{i64}` payload. Check `field_already_optional` / the `optional_wrap`
|
||||
operand type and the `inner_ty` used for `optional_unwrap` vs.
|
||||
`lowerFieldAccessOnType` — the some-branch likely wraps an already-aggregate
|
||||
value, or unwraps to the wrong level for a value-optional. Compare against the
|
||||
working `?*T` path (pointer-optional) to see where the value-optional diverges.
|
||||
Verify with repro (A): expect `7`, no LLVM verification failure. Add
|
||||
`examples/optionals/09xx-optionals-value-optional-chain-read.sx`.
|
||||
|
||||
**(B)** Once (A) is sound: teach the optional-chain read to dispatch a `#get`
|
||||
accessor. The dereferenced (optional-unwrapped, then pointer-deref'd) receiver
|
||||
type may have a getter — `Lowering.getAccessorFor(deref_ty, field)`. In
|
||||
`lowerOptionalChain`'s some-branch, when a getter exists, bind the unwrapped
|
||||
receiver to a synthetic local (see `bindSyntheticLocal` in
|
||||
`src/ir/lower/stmt.zig` for the pattern) and lower a non-optional `tmp.field`
|
||||
read (which hits the existing getter intercept in `lowerFieldAccess`), then wrap
|
||||
as `?R`. Mirror the type in `src/ir/expr_typer.zig` — the `.field_access`
|
||||
optional-chain arm already calls `getAccessorFor` after unwrapping the optional,
|
||||
but it does NOT peel the extra pointer layer for a `?*T` receiver (so
|
||||
`getAccessorFor(*T, ...)` returns null); peel the pointer there too. Verify with
|
||||
repro (B): expect `4`. Add a regression example.
|
||||
|
||||
## Provenance
|
||||
|
||||
Found during the `#set` accessor review (mirrors the `#get` accessor). The
|
||||
`#set`/`#get` work itself is complete and green; this issue is the optional-chain
|
||||
interaction it surfaced. The `#set` write side through `?.` is intentionally left
|
||||
matching real-field behavior (optional-chain assignment unsupported) and is not
|
||||
part of this issue.
|
||||
Reference in New Issue
Block a user