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

@@ -117,6 +117,7 @@ pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
self.builder.currentFunc().is_naked = (fd.abi == .naked);
self.builder.currentFunc().is_get = fd.is_get;
self.builder.currentFunc().is_set = fd.is_set;
// Create entry block
const entry_name = self.module.types.internString("entry");
@@ -1554,7 +1555,9 @@ pub fn genericInstanceMethod(self: *Lowering, inst_name: []const u8, method: []c
/// which is the template's defining module (the author's own method node).
/// Null when the function fails to resolve post-monomorphization.
pub fn ensureGenericInstanceMethodLowered(self: *Lowering, m: GenericStructMethod) ?FuncId {
const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ m.inst_name, m.fd.name }) catch return null;
// A `#set` accessor mangles as `Inst.name$set` so its monomorph never
// collides with the same-name `#get`'s `Inst.name` (coexistence).
const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ m.inst_name, self.accessorEffName(m.fd) }) catch return null;
if (!self.lowered_functions.contains(mangled)) {
self.monomorphizeFunction(m.fd, mangled, m.bindings);
}