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:
@@ -196,6 +196,11 @@ pub const FnDecl = struct {
|
||||
/// Invoked via field syntax (`obj.name`) when no real field matches, rather
|
||||
/// than as a `obj.name()` call. Takes only the `self` receiver.
|
||||
is_get: bool = false,
|
||||
/// `name :: (self: *T, value: V) #set { ... }` — the WRITE counterpart of a
|
||||
/// `#get` accessor. `obj.name = rhs` dispatches to it as `obj.name(rhs)` when
|
||||
/// no real field matches. Takes the `self` receiver plus exactly one value
|
||||
/// parameter and returns void.
|
||||
is_set: bool = false,
|
||||
};
|
||||
|
||||
pub const Param = struct {
|
||||
|
||||
@@ -656,6 +656,11 @@ pub const Function = struct {
|
||||
/// receiver as `self`.
|
||||
is_get: bool = false,
|
||||
|
||||
/// `#set` property accessor (ast.FnDecl.is_set). The write counterpart of
|
||||
/// `is_get`: `obj.name = rhs` dispatches to it as `obj.name(rhs)` when no
|
||||
/// real field matches.
|
||||
is_set: bool = false,
|
||||
|
||||
pub const Param = struct {
|
||||
name: StringId,
|
||||
ty: TypeId,
|
||||
|
||||
@@ -1746,6 +1746,9 @@ pub const Lowering = struct {
|
||||
pub const fnDeclOfRaw = lower_decl.fnDeclOfRaw;
|
||||
pub const structDeclOfRaw = lower_decl.structDeclOfRaw;
|
||||
pub const structMethodFn = lower_decl.structMethodFn;
|
||||
pub const accessorEffName = lower_decl.accessorEffName;
|
||||
pub const accessorNameMatches = lower_decl.accessorNameMatches;
|
||||
pub const setter_eff_suffix = lower_decl.setter_eff_suffix;
|
||||
pub const typeFnAuthor = lower_decl.typeFnAuthor;
|
||||
pub const selectedFuncId = lower_decl.selectedFuncId;
|
||||
pub const bareAuthorFuncId = lower_decl.bareAuthorFuncId;
|
||||
@@ -1968,6 +1971,7 @@ pub const Lowering = struct {
|
||||
pub const lowerInitBlock = lower_expr.lowerInitBlock;
|
||||
pub const getStructFields = lower_expr.getStructFields;
|
||||
pub const getAccessorFor = lower_expr.getAccessorFor;
|
||||
pub const getSetterFor = lower_expr.getSetterFor;
|
||||
pub const fixupMethodReceiver = lower_expr.fixupMethodReceiver;
|
||||
pub const getStructTypeName = lower_expr.getStructTypeName;
|
||||
pub const builtinTypeName = lower_expr.builtinTypeName;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -626,7 +626,10 @@ pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_fil
|
||||
for (sd.methods) |method_node| {
|
||||
if (method_node.data == .fn_decl) {
|
||||
const method_fd = &method_node.data.fn_decl;
|
||||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue;
|
||||
// A `#set` accessor registers under `name$set` so it never
|
||||
// clobbers a same-name `#get` (issue: get+set coexistence).
|
||||
const eff = self.accessorEffName(method_fd);
|
||||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, eff }) catch continue;
|
||||
self.program_index.fn_ast_map.put(qualified, method_fd) catch {};
|
||||
}
|
||||
}
|
||||
@@ -724,8 +727,11 @@ pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_fil
|
||||
for (sd.methods) |method_node| {
|
||||
if (method_node.data == .fn_decl) {
|
||||
const method_fd = &method_node.data.fn_decl;
|
||||
// Build qualified name: StructName.method
|
||||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue;
|
||||
// Build qualified name: StructName.method. A `#set` accessor uses
|
||||
// its `name$set` effective name so a get+set pair keeps two distinct
|
||||
// fn_ast_map slots and two distinct FuncId stubs (coexistence).
|
||||
const eff = self.accessorEffName(method_fd);
|
||||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, eff }) catch continue;
|
||||
self.program_index.fn_ast_map.put(qualified, method_fd) catch {};
|
||||
// Declare extern stub (body is lowered lazily on demand)
|
||||
self.declareFunction(method_fd, qualified);
|
||||
|
||||
@@ -951,6 +951,7 @@ pub fn monomorphizePackFn(
|
||||
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;
|
||||
|
||||
const entry_name = self.module.types.internString("entry");
|
||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||
|
||||
@@ -582,6 +582,166 @@ fn rootIsConstant(self: *Lowering, root: []const u8) bool {
|
||||
};
|
||||
}
|
||||
|
||||
/// Map a compound-assignment op to the binary op it folds with, for the
|
||||
/// get-modify-set rewrite of `obj.prop OP= x` (a `#set` property).
|
||||
fn compoundAssignToBinaryOp(op: ast.Assignment.Op) ast.BinaryOp.Op {
|
||||
return switch (op) {
|
||||
.add_assign => .add,
|
||||
.sub_assign => .sub,
|
||||
.mul_assign => .mul,
|
||||
.div_assign => .div,
|
||||
.mod_assign => .mod,
|
||||
.and_assign => .bit_and,
|
||||
.or_assign => .bit_or,
|
||||
.xor_assign => .bit_xor,
|
||||
.shl_assign => .shl,
|
||||
.shr_assign => .shr,
|
||||
.assign => unreachable, // plain assign never reaches the rewrite
|
||||
};
|
||||
}
|
||||
|
||||
/// Bind an already-lowered `Ref` (`val` of type `ty`) to a fresh, unspellable
|
||||
/// (`$`-prefixed) local and return an identifier node that resolves to it. Lets
|
||||
/// a synthesized accessor call reference a pre-computed receiver/value WITHOUT
|
||||
/// re-lowering it — the basis for single-eval property writes. Null when there
|
||||
/// is no scope to bind into.
|
||||
fn bindSyntheticLocal(self: *Lowering, prefix: []const u8, val: Ref, ty: TypeId, span: ast.Span) ?*Node {
|
||||
const s = self.scope orelse return null;
|
||||
var namebuf: [48]u8 = undefined;
|
||||
const tmp = std.fmt.bufPrint(&namebuf, "${s}_{d}", .{ prefix, self.block_counter }) catch prefix;
|
||||
self.block_counter += 1;
|
||||
const owned = self.alloc.dupe(u8, tmp) catch return null;
|
||||
s.put(owned, .{ .ref = val, .ty = ty, .is_alloca = false });
|
||||
const id = self.alloc.create(Node) catch return null;
|
||||
id.* = .{ .span = span, .data = .{ .identifier = .{ .name = owned } } };
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Synthesize and lower `recv_obj.<setter-effective-name>(value_node)` — the
|
||||
/// shared tail of every `#set` dispatch.
|
||||
fn emitSetterCall(self: *Lowering, recv_obj: *Node, setter: *const ast.FnDecl, value_node: *Node, span: ast.Span) void {
|
||||
const callee = self.alloc.create(Node) catch return;
|
||||
callee.* = .{ .span = span, .data = .{ .field_access = .{ .object = recv_obj, .field = self.accessorEffName(setter) } } };
|
||||
const args = self.alloc.alloc(*Node, 1) catch return;
|
||||
args[0] = value_node;
|
||||
const syn_call = ast.Call{ .callee = callee, .args = args };
|
||||
_ = self.lowerCall(&syn_call);
|
||||
}
|
||||
|
||||
/// `<fa> = <already-lowered val>` where `prop` is a `#set` property and the RHS
|
||||
/// `Ref` was computed by the caller (the multi-assign path evaluates ALL RHS
|
||||
/// values up front, so re-lowering would double-evaluate and break ordering).
|
||||
/// Binds `val` to a synthetic local and dispatches the setter through it.
|
||||
/// Returns true when it consumed the store (setter write, or a read-only
|
||||
/// diagnostic for a `#get`-only property); false for an ordinary field.
|
||||
fn tryLowerPropertyStore(self: *Lowering, fa: ast.FieldAccess, val: Ref, span: ast.Span) bool {
|
||||
var recv_ty = self.inferExprType(fa.object);
|
||||
if (!recv_ty.isBuiltin()) {
|
||||
const di = self.module.types.get(recv_ty);
|
||||
if (di == .pointer) recv_ty = di.pointer.pointee;
|
||||
}
|
||||
if (recv_ty.isBuiltin()) return false;
|
||||
const setter = self.getSetterFor(recv_ty, fa.field) orelse {
|
||||
if (self.getAccessorFor(recv_ty, fa.field) != null) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, span, "property '{s}' is read-only (no '#set')", .{fa.field});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
var recv_obj: *Node = fa.object;
|
||||
if (fa.object.data == .deref_expr) recv_obj = fa.object.data.deref_expr.operand;
|
||||
const val_id = bindSyntheticLocal(self, "prop_val", val, self.builder.getRefType(val), span) orelse return false;
|
||||
emitSetterCall(self, recv_obj, setter, val_id, span);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// `obj.prop = rhs` (or `obj.prop OP= rhs`) where `prop` is a `#set` property
|
||||
/// accessor. Dispatches to the setter as `obj.prop$set(rhs)` — the write
|
||||
/// counterpart of the `#get` read dispatch in `lowerFieldAccess`. Returns true
|
||||
/// when it consumed the assignment (a real setter write, or a clean
|
||||
/// read-only/write-only diagnostic); false to let normal field-store lowering
|
||||
/// proceed (an ordinary field, or no property at all).
|
||||
///
|
||||
/// Must run BEFORE `lowerAssignment` lowers the RHS: a plain-assign setter call
|
||||
/// lowers `rhs` itself (once, with the setter's value-param type as target), so
|
||||
/// pre-lowering it here would double-evaluate.
|
||||
fn tryLowerPropertyAssignment(self: *Lowering, asgn: *const ast.Assignment) bool {
|
||||
const fa = asgn.target.data.field_access;
|
||||
// Dereference the receiver type down to the struct that owns the accessor.
|
||||
var recv_ty = self.inferExprType(fa.object);
|
||||
if (!recv_ty.isBuiltin()) {
|
||||
const di = self.module.types.get(recv_ty);
|
||||
if (di == .pointer) recv_ty = di.pointer.pointee;
|
||||
}
|
||||
if (recv_ty.isBuiltin()) return false;
|
||||
|
||||
const setter = self.getSetterFor(recv_ty, fa.field);
|
||||
const getter = self.getAccessorFor(recv_ty, fa.field);
|
||||
|
||||
if (setter == null) {
|
||||
// No setter. A same-name `#get` (with no real field — getAccessorFor
|
||||
// guarantees a real field wins) means the property is read-only: reject
|
||||
// the write with a clear message rather than "field not found".
|
||||
if (getter != null) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, asgn.target.span, "property '{s}' is read-only (no '#set')", .{fa.field});
|
||||
return true;
|
||||
}
|
||||
return false; // ordinary field, or not a property → normal store path
|
||||
}
|
||||
|
||||
// The receiver node the synthesized get/set dispatch on. An explicit-deref
|
||||
// receiver `(*p).prop` dispatches on the inner pointer `p` (auto-deref takes
|
||||
// the working path).
|
||||
var recv_obj: *Node = fa.object;
|
||||
if (fa.object.data == .deref_expr) recv_obj = fa.object.data.deref_expr.operand;
|
||||
|
||||
// For a compound `OP=`, the receiver is read (via `#get`) AND written (via
|
||||
// `#set`), so it must be evaluated EXACTLY ONCE — otherwise a side-effecting
|
||||
// receiver (`next().prop += 1`) reads one object and writes another. Bind
|
||||
// the receiver's `*T` to a synthetic, unspellable local and dispatch both
|
||||
// the read and the write on it. (A plain assign's single setter call already
|
||||
// evaluates the receiver once, so it keeps using the original node.)
|
||||
if (asgn.op != .assign) {
|
||||
if (getter == null) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, asgn.target.span, "property '{s}' is write-only (no '#get'); compound assignment needs to read the current value", .{fa.field});
|
||||
return true;
|
||||
}
|
||||
// Evaluate the receiver once into a synthetic `*T` binding. `*T` receiver
|
||||
// → the pointer value itself; a `T` lvalue → its address (so the setter
|
||||
// mutates the original, not a copy). Guarded on a scope being present;
|
||||
// without one (e.g. a top-level init) fall back to the original node —
|
||||
// the receiver re-lowers, but functionality is preserved.
|
||||
if (self.scope != null) {
|
||||
var ptr_ty = self.inferExprType(recv_obj);
|
||||
const is_ptr = !ptr_ty.isBuiltin() and self.module.types.get(ptr_ty) == .pointer;
|
||||
const recv_ptr = if (is_ptr) self.lowerExpr(recv_obj) else self.lowerExprAsPtr(recv_obj);
|
||||
if (!is_ptr) ptr_ty = self.module.types.ptrTo(ptr_ty);
|
||||
if (bindSyntheticLocal(self, "prop_recv", recv_ptr, ptr_ty, asgn.target.span)) |id| recv_obj = id;
|
||||
}
|
||||
}
|
||||
|
||||
// The value the setter receives. For a compound `OP=`: `(recv.prop) OP rhs`
|
||||
// — the read dispatches to the `#get` on the (now single-eval) receiver.
|
||||
var value_node: *Node = asgn.value;
|
||||
if (asgn.op != .assign) {
|
||||
const read_node = self.alloc.create(Node) catch return false;
|
||||
read_node.* = .{ .span = asgn.target.span, .data = .{ .field_access = .{ .object = recv_obj, .field = fa.field } } };
|
||||
const bin_node = self.alloc.create(Node) catch return false;
|
||||
bin_node.* = .{ .span = asgn.value.span, .data = .{ .binary_op = .{
|
||||
.op = compoundAssignToBinaryOp(asgn.op),
|
||||
.lhs = read_node,
|
||||
.rhs = asgn.value,
|
||||
} } };
|
||||
value_node = bin_node;
|
||||
}
|
||||
|
||||
emitSetterCall(self, recv_obj, setter.?, value_node, asgn.target.span);
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||
// Writes through a constant are rejected at compile time (issue 0116):
|
||||
// the target chain's root naming a const global (array/struct consts,
|
||||
@@ -596,6 +756,13 @@ pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// `#set` property accessor: `obj.prop = rhs` (or `OP=`) dispatches to the
|
||||
// setter as `obj.prop$set(rhs)`. Must run before the RHS is lowered below
|
||||
// (the synthesized call lowers it itself). Falls through for ordinary fields.
|
||||
if (asgn.target.data == .field_access) {
|
||||
if (tryLowerPropertyAssignment(self, asgn)) return;
|
||||
}
|
||||
|
||||
// Set target_type from LHS for RHS lowering (enum literals, struct literals, etc.)
|
||||
const old_target = self.target_type;
|
||||
if (asgn.target.data == .identifier) {
|
||||
@@ -1445,6 +1612,10 @@ pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
||||
}
|
||||
},
|
||||
.field_access => |fa| {
|
||||
// `#set` property target: dispatch to the setter with the
|
||||
// already-lowered RHS value (multi-assign evaluated all RHS up
|
||||
// front). Falls through for an ordinary field.
|
||||
if (tryLowerPropertyStore(self, fa, val, target.span)) continue;
|
||||
const obj_ptr = self.lowerExprAsPtr(fa.object);
|
||||
const obj_ty = self.inferExprType(fa.object);
|
||||
// Reject a direct write to a tagged-union variant (issue 0136).
|
||||
|
||||
@@ -113,6 +113,7 @@ pub const Lexer = struct {
|
||||
.{ "#selector", Tag.hash_selector },
|
||||
.{ "#property", Tag.hash_property },
|
||||
.{ "#get", Tag.hash_get },
|
||||
.{ "#set", Tag.hash_set },
|
||||
.{ "#caller_location", Tag.hash_caller_location },
|
||||
};
|
||||
inline for (directives) |d| {
|
||||
|
||||
@@ -1716,6 +1716,7 @@ pub const Server = struct {
|
||||
.hash_selector,
|
||||
.hash_property,
|
||||
.hash_get,
|
||||
.hash_set,
|
||||
.hash_caller_location,
|
||||
=> ST.keyword,
|
||||
|
||||
|
||||
@@ -1958,12 +1958,25 @@ pub const Parser = struct {
|
||||
return_type = try self.parseTypeExpr();
|
||||
}
|
||||
|
||||
// Optional `#get` property-accessor marker: `name :: (self) -> R #get => expr;`.
|
||||
// The method is invoked via field syntax (`obj.name`) rather than `obj.name()`.
|
||||
// Optional `#get` / `#set` property-accessor marker:
|
||||
// read: `name :: (self) -> R #get => expr;` (invoked via `obj.name`)
|
||||
// write: `name :: (self, value: V) #set { … }` (invoked via `obj.name = rhs`)
|
||||
// The two share the marker slot; a `#set` has no return type (void) and
|
||||
// takes the receiver plus exactly one value parameter.
|
||||
var is_get = false;
|
||||
var is_set = false;
|
||||
if (self.current.tag == .hash_get) {
|
||||
is_get = true;
|
||||
self.advance();
|
||||
} else if (self.current.tag == .hash_set) {
|
||||
is_set = true;
|
||||
self.advance();
|
||||
if (return_type != null)
|
||||
return self.fail("a '#set' accessor returns void — drop the '-> T' return type");
|
||||
// self + exactly one value parameter. `params` here are the value/
|
||||
// receiver params only (type params `$T` are collected separately).
|
||||
if (params.len != 2)
|
||||
return self.fail("a '#set' accessor takes exactly the receiver and one value parameter");
|
||||
}
|
||||
|
||||
// Optional ABI / calling-convention annotation: `abi(.c)` / `abi(.zig)` /
|
||||
@@ -2057,6 +2070,7 @@ pub const Parser = struct {
|
||||
.name_span = name_span,
|
||||
.is_raw = name_is_raw,
|
||||
.is_get = is_get,
|
||||
.is_set = is_set,
|
||||
} });
|
||||
}
|
||||
|
||||
@@ -3775,7 +3789,9 @@ pub const Parser = struct {
|
||||
if (tag == .arrow) return self.hasFnBodyAfterArrow();
|
||||
// `kw_extern`/`kw_export`: a postfix linkage modifier (e.g. `f :: () extern;`
|
||||
// with no return type) marks a fn decl just like `abi(...)`.
|
||||
return tag == .l_brace or tag == .hash_builtin or tag == .fat_arrow or tag == .kw_abi or tag == .kw_extern or tag == .kw_export;
|
||||
// `#set` is a bodied accessor with NO return type, so it sits directly
|
||||
// after `)` (`(self, v) #set { … }`) — a fn-def marker like `{`/`=>`.
|
||||
return tag == .l_brace or tag == .hash_builtin or tag == .fat_arrow or tag == .hash_set or tag == .kw_abi or tag == .kw_extern or tag == .kw_export;
|
||||
}
|
||||
|
||||
fn hasFnBodyAfterArrow(self: *Parser) bool {
|
||||
@@ -3803,6 +3819,7 @@ pub const Parser = struct {
|
||||
if (self.current.tag == .l_brace) return true;
|
||||
if (self.current.tag == .hash_builtin) return true;
|
||||
if (self.current.tag == .hash_get) return true; // `-> R #get => …` is a fn def
|
||||
if (self.current.tag == .hash_set) return true; // `-> R #set { … }` is a fn def
|
||||
if (self.current.tag == .kw_abi) return true;
|
||||
// Postfix linkage modifier after the return type: `-> R extern;` /
|
||||
// `-> R export { … }` (and `-> R abi(.c) extern`). Marks a fn def.
|
||||
|
||||
@@ -142,6 +142,7 @@ pub const Tag = enum {
|
||||
hash_selector, // `#selector("explicit:string")` per-method Obj-C selector override (Phase 3.2)
|
||||
hash_property, // `#property[(modifier, ...)]` field directive — synthesizes getter/setter dispatch (M2.2)
|
||||
hash_get, // `name :: (self) -> R #get => expr;` — a no-paren property accessor method (read via field syntax)
|
||||
hash_set, // `name :: (self, value) #set { ... }` — the write counterpart of #get (`obj.name = rhs` dispatches here)
|
||||
hash_caller_location, // `#caller_location` — as a param default, synthesizes the call site's Source_Location (ERR E4.1b)
|
||||
hash_jni_env, // `#jni_env(env) { body }` block-form env-scoping intrinsic
|
||||
hash_jni_main, // `#jni_main #jni_class(...) { ... }` — class is the launchable Android Activity
|
||||
|
||||
Reference in New Issue
Block a user