Files
sx/issues/0145-method-on-array-index-receiver-copies-element.md
agra d4edf4b4b0 fix: method on array-index/deref receiver mutates the live place (issue 0145)
A *self method called directly on arr[i] (or a deref place) fell through to an
alloca+store-of-value, so the callee mutated a throwaway copy and the live slot
was never written. fixupMethodReceiver now takes the real address of
.index_expr/.deref_expr receivers via lowerExprAsPtr (normalized to *T),
mirroring the explicit-argument path. A comptime-pack index (xs[i] where xs is
a pack) is excluded -- a pack has no runtime storage to address -- so it keeps
flowing through the general copy path.

Regression: examples/0188-types-method-array-index-receiver.sx
2026-06-21 09:11:44 +03:00

79 lines
2.6 KiB
Markdown

# 0145 — method with `*self` called directly on an array-index expression operates on a COPY
> **RESOLVED.** `fixupMethodReceiver` (src/ir/lower/expr.zig) now takes the
> real address of `.index_expr` and `.deref_expr` receivers via
> `lowerExprAsPtr` (normalizing to `*T`), mirroring the explicit-argument path
> in `call.zig` — so `arr[i].method()` mutates the live slot instead of a
> throwaway copy. A comptime-pack index (`xs[i]` where `xs` is a pack) is
> explicitly excluded: a pack has no runtime storage to address, so it keeps
> flowing through the general alloca+store-of-value path. Regression test:
> `examples/0188-types-method-array-index-receiver.sx`.
## Summary
Calling a method whose receiver is `*self` (mutating) directly on a
fixed-array element expression — `arr[i].method(...)` — mutates a temporary
COPY of the element, not the live array slot. The mutation is silently lost.
Binding the element to a pointer first (`p := @arr[i]; p.method(...)`) works
correctly.
## Repro
```
S :: struct {
flag: bool;
set :: (self: *S) { self.flag = true; }
}
A :: struct { items: [4]S; }
main :: () -> i32 {
a : A = .{};
a.items[1].set(); // BUG: mutates a copy
print("direct = {}\n", a.items[1].flag); // prints false
p := @a.items[1];
p.set(); // OK: mutates the live slot
print("ptr = {}\n", a.items[1].flag); // prints true
0
}
```
Observed:
```
direct = false
ptr = true
```
Expected: both print `true``a.items[1].set()` takes `*self` and should bind
the receiver to the address of `a.items[1]`, exactly as the explicit-pointer
form does.
The same surfaced with a non-trivial method (`Slider.handle_event(self: *Slider,
...)`): the direct call returned `true` (so the method body ran) yet left the
element's `pressed`/`value` fields unchanged, while `@arr[i]` bound to a local
and called on that pointer mutated the element as expected.
## Impact
Any struct that holds a fixed array of widgets/records and dispatches `*self`
methods per element (a layers panel with one `Slider` per row, an entity table,
etc.) silently no-ops the mutation. It is easy to miss because the method's
return value is correct — only the in-place writes vanish.
## Workaround
Bind the element to a pointer before the call:
```
p := @arr[i];
p.method(...);
```
Field stores through the index (`arr[i].field = v`) and value assignment
(`arr[i] = v`) appear unaffected; only the implicit `&arr[i]` receiver of a
`*self` method call is materialized as a copy.
Found while implementing `ui/layers_panel.sx` in the photo editor (one opacity
`Slider` per layer row).