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:
@@ -881,6 +881,37 @@ pub fn getAccessorFor(self: *Lowering, ty: TypeId, field: []const u8) ?*const as
|
||||
return null;
|
||||
}
|
||||
|
||||
/// A `#set` property accessor for `obj_ty.field`, or null — the WRITE
|
||||
/// counterpart of `getAccessorFor`. A `#set` is registered/dispatched under its
|
||||
/// effective `field$set` name (so a same-name `#get` keeps the plain `field`),
|
||||
/// and a REAL field of the same name wins over it (parallels the `#get` rule).
|
||||
/// `ty` must be the dereferenced (non-pointer) receiver type.
|
||||
pub fn getSetterFor(self: *Lowering, ty: TypeId, field: []const u8) ?*const ast.FnDecl {
|
||||
if (ty.isBuiltin()) return null;
|
||||
// A REAL field of this name wins over a same-name `#set` (a setter must not
|
||||
// shadow stored data on the write path).
|
||||
const field_id = self.module.types.internString(field);
|
||||
for (self.getStructFields(ty)) |f| {
|
||||
if (f.name == field_id) return null;
|
||||
}
|
||||
const eff = std.fmt.allocPrint(self.alloc, "{s}" ++ Lowering.setter_eff_suffix, .{field}) catch return null;
|
||||
// Generic instance: keyed by the instance name (e.g. "List(i64)").
|
||||
const tn = self.formatTypeName(ty);
|
||||
if (self.genericInstanceMethod(tn, eff)) |m| {
|
||||
return if (m.fd.is_set) m.fd else null;
|
||||
}
|
||||
// Plain struct: the setter stub is registered "StructName.field$set".
|
||||
const info = self.module.types.get(ty);
|
||||
if (info == .@"struct") {
|
||||
const sname = self.module.types.getString(info.@"struct".name);
|
||||
const q = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, eff }) catch return null;
|
||||
if (self.program_index.fn_ast_map.get(q)) |fd| {
|
||||
return if (fd.is_set) fd else null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref {
|
||||
const field_name_id = self.module.types.internString(field);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user