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:
@@ -2102,9 +2102,41 @@ pub const VisibleStructAuthor = struct {
|
||||
/// the bare-visible author's own method (`b.Box.make`), bypassing the name-keyed
|
||||
/// last-wins `fn_ast_map` ("Box.make") that a 2-flat-hop same-name template's
|
||||
/// method would otherwise win (E4 #1, static-method site).
|
||||
/// The suffix that distinguishes a `#set` accessor's EFFECTIVE method name from
|
||||
/// the read name it shares with a same-name `#get`. `$` can never appear in an
|
||||
/// sx identifier (it is the comptime-param sigil), so `len$set` is an
|
||||
/// unmistakable, symbol-safe key that cannot collide with any user method name
|
||||
/// — yet it keeps the getter under the plain `len`, so registration / mangling /
|
||||
/// dispatch keep BOTH accessors of a get+set pair distinct. See
|
||||
/// `accessorEffName` / `accessorNameMatches`.
|
||||
pub const setter_eff_suffix = "$set";
|
||||
|
||||
/// The name a method is REGISTERED / MANGLED / DISPATCHED under: a `#set`
|
||||
/// accessor is keyed as `name$set` so it never clobbers the same-name `#get`
|
||||
/// (which keeps its plain `name`); every other method keeps its own name.
|
||||
pub fn accessorEffName(self: *Lowering, fd: *const ast.FnDecl) []const u8 {
|
||||
if (!fd.is_set) return fd.name;
|
||||
return std.fmt.allocPrint(self.alloc, "{s}" ++ setter_eff_suffix, .{fd.name}) catch fd.name;
|
||||
}
|
||||
|
||||
/// True when method `fd` is the one a name-keyed lookup for `query` should
|
||||
/// resolve to. A `name$set` query resolves ONLY the `#set` accessor named
|
||||
/// `name`; a plain `name` query resolves any NON-setter (a `#get` accessor or an
|
||||
/// ordinary method), never a setter. This makes get/set coexistence
|
||||
/// declaration-order-independent (the read query picks the getter, the
|
||||
/// `…$set` write query picks the setter) without an overload table.
|
||||
pub fn accessorNameMatches(fd: *const ast.FnDecl, query: []const u8) bool {
|
||||
if (std.mem.endsWith(u8, query, setter_eff_suffix)) {
|
||||
if (!fd.is_set) return false;
|
||||
return std.mem.eql(u8, fd.name, query[0 .. query.len - setter_eff_suffix.len]);
|
||||
}
|
||||
if (fd.is_set) return false;
|
||||
return std.mem.eql(u8, fd.name, query);
|
||||
}
|
||||
|
||||
pub fn structMethodFn(sd: *const ast.StructDecl, method: []const u8) ?*const ast.FnDecl {
|
||||
for (sd.methods) |mn| {
|
||||
if (mn.data == .fn_decl and std.mem.eql(u8, mn.data.fn_decl.name, method))
|
||||
if (mn.data == .fn_decl and accessorNameMatches(&mn.data.fn_decl, method))
|
||||
return &mn.data.fn_decl;
|
||||
}
|
||||
return null;
|
||||
@@ -2323,6 +2355,7 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
|
||||
func.has_implicit_ctx = wants_ctx;
|
||||
func.is_naked = (fd.abi == .naked);
|
||||
func.is_get = fd.is_get;
|
||||
func.is_set = fd.is_set;
|
||||
self.extern_name_map.put(name, c_name) catch {};
|
||||
self.fn_decl_fids.put(fd, fid) catch {};
|
||||
return;
|
||||
@@ -2338,6 +2371,7 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
|
||||
func.has_implicit_ctx = wants_ctx;
|
||||
func.is_naked = (fd.abi == .naked);
|
||||
func.is_get = fd.is_get;
|
||||
func.is_set = fd.is_set;
|
||||
if (weldedCompilerFn(self, fd, name)) func.compiler_welded = true;
|
||||
// A BODIED `abi(.compiler)` function is a user compiler-domain function (e.g. a
|
||||
// post-link callback): the VM runs its sx body, but it NEVER runs in the binary
|
||||
|
||||
Reference in New Issue
Block a user