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:
agra
2026-06-22 17:55:18 +03:00
parent 5cc45a2b38
commit 9523c29173
36 changed files with 526 additions and 19 deletions

View File

@@ -2,7 +2,7 @@
// live element count, so a List is directly iterable with a `for`-each, and
// `xs.len` reads the live count via a `#get` accessor. Exercises append (incl.
// a realloc past the initial cap of 4), for-each, parallel for-with-index,
// empty iteration, direct `for xs` over the List, and truncation via items.len.
// empty iteration, direct `for xs` over the List, and truncation via `xs.len = 0`.
#import "modules/std.sx";
main :: () -> i64 {
@@ -33,8 +33,8 @@ main :: () -> i64 {
while j < xs.len { acc = acc + xs.items[j]; j = j + 1; }
print("indexed sum={}\n", acc); // 210
// truncate to empty via items.len, then iterate (zero iterations)
xs.items.len = 0;
// truncate to empty via the `len` #set accessor, then iterate (zero iters)
xs.len = 0;
cnt := 0;
for xs.items (e) { cnt = cnt + 1; }
print("after trunc: len={} iters={}\n", xs.len, cnt); // len=0 iters=0

View File

@@ -0,0 +1,21 @@
// `List(T).len` is a `#get`/`#set` property pair: `xs.len` reads the live
// element count (delegating to `items.len`), and `xs.len = n` sets it (e.g.
// `xs.len = 0` to clear the list without freeing its buffer — `cap` and the
// backing allocation are untouched, so appends reuse the same storage).
#import "modules/std.sx";
main :: () -> i64 {
xs : List(i64) = .{};
xs.append(10);
xs.append(20);
xs.append(30);
print("len={} cap={}\n", xs.len, xs.cap); // len=3 cap=4
xs.len = 0; // clear via the #set property
print("after clear: len={} cap={}\n", xs.len, xs.cap); // len=0 cap=4
// The buffer survived the clear — re-append reuses it (cap stays 4).
xs.append(99);
print("reused: len={} cap={} first={}\n", xs.len, xs.cap, xs.items[0]); // 1 4 99
return 0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
len=3 cap=4
after clear: len=0 cap=4
reused: len=1 cap=4 first=99