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

2.6 KiB

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 truea.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).