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:
@@ -0,0 +1,17 @@
|
||||
// Writing to a `#get`-only property (no matching `#set`) is rejected with a
|
||||
// clear "read-only" diagnostic — not the generic "field not found" the bare
|
||||
// struct-store path would emit. (The write counterpart, a `#set`-only
|
||||
// property, accepts plain assignment but rejects compound `+=` because there is
|
||||
// no `#get` to read the current value.)
|
||||
#import "modules/std.sx";
|
||||
|
||||
Reading :: struct {
|
||||
raw: i64 = 0;
|
||||
doubled :: (self: *Reading) -> i64 #get => self.raw * 2;
|
||||
}
|
||||
|
||||
main :: () -> i64 {
|
||||
r : Reading = .{ raw = 5 };
|
||||
r.doubled = 10; // ERROR: property 'doubled' is read-only (no '#set')
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
error: property 'doubled' is read-only (no '#set')
|
||||
--> examples/diagnostics/1193-diagnostics-readonly-property-write.sx:15:5
|
||||
|
|
||||
15 | r.doubled = 10; // ERROR: property 'doubled' is read-only (no '#set')
|
||||
| ^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
|
||||
21
examples/memory/0841-memory-list-len-set.sx
Normal file
21
examples/memory/0841-memory-list-len-set.sx
Normal 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;
|
||||
}
|
||||
1
examples/memory/expected/0841-memory-list-len-set.exit
Normal file
1
examples/memory/expected/0841-memory-list-len-set.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/memory/expected/0841-memory-list-len-set.stderr
Normal file
1
examples/memory/expected/0841-memory-list-len-set.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
3
examples/memory/expected/0841-memory-list-len-set.stdout
Normal file
3
examples/memory/expected/0841-memory-list-len-set.stdout
Normal file
@@ -0,0 +1,3 @@
|
||||
len=3 cap=4
|
||||
after clear: len=0 cap=4
|
||||
reused: len=1 cap=4 first=99
|
||||
63
examples/types/0198-types-set-property-accessor.sx
Normal file
63
examples/types/0198-types-set-property-accessor.sx
Normal file
@@ -0,0 +1,63 @@
|
||||
// `#set` property accessors — the write counterpart of `#get`. A method
|
||||
// `name :: (self: *T, value: V) #set { ... }` is invoked via field-assign
|
||||
// syntax (`obj.name = rhs`) rather than `obj.name(rhs)`. A property may carry
|
||||
// BOTH a `#get` and a `#set` of the same name (reads pick the getter, writes
|
||||
// pick the setter); compound assignment (`+=`) reads via `#get` then writes via
|
||||
// `#set`. Works on plain structs and generic-struct instances, as a multi-assign
|
||||
// target, and the compound form evaluates the receiver exactly once.
|
||||
#import "modules/std.sx";
|
||||
|
||||
// get + set pair on the same name, with a scaling setter so we can see which
|
||||
// path fired.
|
||||
Temp :: struct {
|
||||
raw: i64 = 0;
|
||||
celsius :: (self: *Temp) -> i64 #get => self.raw;
|
||||
celsius :: (self: *Temp, v: i64) #set { self.raw = v; }
|
||||
}
|
||||
|
||||
// Generic instance: setter takes the type parameter as its value type.
|
||||
Box :: struct ($T: Type) {
|
||||
slot: T;
|
||||
val :: (self: *Box(T)) -> T #get => self.slot;
|
||||
val :: (self: *Box(T), v: T) #set { self.slot = v; }
|
||||
}
|
||||
|
||||
main :: () -> i64 {
|
||||
t : Temp = .{};
|
||||
t.celsius = 30; // setter
|
||||
print("celsius={}\n", t.celsius); // 30 (getter)
|
||||
t.celsius += 5; // get-modify-set
|
||||
print("after +=5: {}\n", t.celsius); // 35
|
||||
t.celsius *= 2; // get-modify-set
|
||||
print("after *=2: {}\n", t.celsius); // 70
|
||||
|
||||
b : Box(i64) = .{ slot = 1 };
|
||||
b.val = 99; // setter (value type is T = i64)
|
||||
print("box={}\n", b.val); // 99
|
||||
b.val -= 9;
|
||||
print("box={}\n", b.val); // 90
|
||||
|
||||
// Multi-assign with property targets — a swap proves all RHS values are
|
||||
// evaluated before any setter fires.
|
||||
p : Temp = .{ raw = 1 };
|
||||
q : Temp = .{ raw = 2 };
|
||||
p.celsius, q.celsius = q.celsius, p.celsius;
|
||||
print("swap: p={} q={}\n", p.celsius, q.celsius); // 2 1
|
||||
|
||||
// Compound assign through a property evaluates the receiver EXACTLY ONCE:
|
||||
// a moving receiver reads and writes the SAME element (not two different ones).
|
||||
g_idx = 0;
|
||||
cells[0] = .{ raw = 10 };
|
||||
cells[1] = .{ raw = 20 };
|
||||
next_cell().celsius += 1; // reads & writes cells[0]
|
||||
print("once: c0={} c1={} idx={}\n", cells[0].celsius, cells[1].celsius, g_idx); // 11 20 1
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_idx : i64 = 0;
|
||||
cells : [2]Temp = .[ .{}, .{} ];
|
||||
next_cell :: () -> *Temp {
|
||||
cur := g_idx;
|
||||
g_idx = g_idx + 1;
|
||||
return @cells[cur];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
celsius=30
|
||||
after +=5: 35
|
||||
after *=2: 70
|
||||
box=99
|
||||
box=90
|
||||
swap: p=2 q=1
|
||||
once: c0=11 c1=20 idx=1
|
||||
92
issues/0160-optional-chain-value-optional-and-accessors.md
Normal file
92
issues/0160-optional-chain-value-optional-and-accessors.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# 0160 — optional-chain field access: `?T` value-optional read miscompiles, and `#get`/`#set` accessors aren't reached through `?.`
|
||||
|
||||
## Symptom
|
||||
|
||||
Two related gaps in optional-chain field access (`obj?.field`), surfaced while
|
||||
extending property accessors (`#get`/`#set`) — but the root problem (A) is
|
||||
PRE-EXISTING and independent of accessors:
|
||||
|
||||
- **(A) `?T` value-optional read of a real field miscompiles.** `ot?.raw` where
|
||||
`ot : ?T` (optional of a *value* struct) fails LLVM verification:
|
||||
`Invalid InsertValueInst operands! ... insertvalue { { i64 }, i1 } undef, { { i64 }, i1 } %si, 0`
|
||||
— the some-branch builds the result optional by inserting the WHOLE
|
||||
`{payload, has_value}` aggregate where the bare payload is expected.
|
||||
Observed: LLVM verification failure (compile abort). Expected: prints `7`.
|
||||
*(The `?*T` pointer-optional form of the same read works correctly, so the
|
||||
bug is specific to value-optionals.)*
|
||||
|
||||
- **(B) `#get`/`#set` accessors are not reached through `?.`.** `pt?.p` where
|
||||
`p` is a `#get` accessor gives `field 'p' not found on type '*T'` — the
|
||||
optional-chain read path (`lowerOptionalChain`) resolves only real fields, not
|
||||
accessors. (The write form `obj?.p = x` is consistent with real fields, which
|
||||
also reject optional-chain assignment, so the write side is NOT part of this
|
||||
issue.)
|
||||
|
||||
(B) is blocked on (A): a correct `obj?.getter` read must run the getter inside
|
||||
the optional's some-branch and re-wrap the result as `?R`, i.e. it reuses the
|
||||
exact some-branch/merge optional-construction path that (A) miscompiles for
|
||||
value-optionals. Layering accessor dispatch onto that path while it miscompiles
|
||||
would bake the same bug into accessors.
|
||||
|
||||
## Reproduction
|
||||
|
||||
(A) — value-optional real-field read (LLVM verify failure):
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
T :: struct { raw: i64 = 7; }
|
||||
main :: () {
|
||||
ot : ?T = .{ raw = 7 };
|
||||
print("{}\n", ot?.raw); // expected: 7 — actual: LLVM verification failure
|
||||
}
|
||||
```
|
||||
|
||||
(B) — accessor through optional chain (field-not-found):
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
T :: struct {
|
||||
raw: i64 = 0;
|
||||
p :: (self: *T) -> i64 #get => self.raw;
|
||||
}
|
||||
main :: () {
|
||||
t : T = .{ raw = 4 };
|
||||
pt : ?*T = @t;
|
||||
print("{}\n", pt?.p); // expected: 4 — actual: field 'p' not found on type '*T'
|
||||
}
|
||||
```
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
Fix (A) first; (B) builds on it.
|
||||
|
||||
**(A)** In `src/ir/lower/expr.zig` `lowerOptionalChain` (the some-branch around
|
||||
the `optional_wrap` of `field_val`): when the optional's child is a *value*
|
||||
struct (`?T`, not `?*T`), the result optional is mis-assembled — the verifier
|
||||
sees a `{ {i64}, i1 }` inserted into slot 0 of `{ {i64}, i1 }` instead of the
|
||||
bare `{i64}` payload. Check `field_already_optional` / the `optional_wrap`
|
||||
operand type and the `inner_ty` used for `optional_unwrap` vs.
|
||||
`lowerFieldAccessOnType` — the some-branch likely wraps an already-aggregate
|
||||
value, or unwraps to the wrong level for a value-optional. Compare against the
|
||||
working `?*T` path (pointer-optional) to see where the value-optional diverges.
|
||||
Verify with repro (A): expect `7`, no LLVM verification failure. Add
|
||||
`examples/optionals/09xx-optionals-value-optional-chain-read.sx`.
|
||||
|
||||
**(B)** Once (A) is sound: teach the optional-chain read to dispatch a `#get`
|
||||
accessor. The dereferenced (optional-unwrapped, then pointer-deref'd) receiver
|
||||
type may have a getter — `Lowering.getAccessorFor(deref_ty, field)`. In
|
||||
`lowerOptionalChain`'s some-branch, when a getter exists, bind the unwrapped
|
||||
receiver to a synthetic local (see `bindSyntheticLocal` in
|
||||
`src/ir/lower/stmt.zig` for the pattern) and lower a non-optional `tmp.field`
|
||||
read (which hits the existing getter intercept in `lowerFieldAccess`), then wrap
|
||||
as `?R`. Mirror the type in `src/ir/expr_typer.zig` — the `.field_access`
|
||||
optional-chain arm already calls `getAccessorFor` after unwrapping the optional,
|
||||
but it does NOT peel the extra pointer layer for a `?*T` receiver (so
|
||||
`getAccessorFor(*T, ...)` returns null); peel the pointer there too. Verify with
|
||||
repro (B): expect `4`. Add a regression example.
|
||||
|
||||
## Provenance
|
||||
|
||||
Found during the `#set` accessor review (mirrors the `#get` accessor). The
|
||||
`#set`/`#get` work itself is complete and green; this issue is the optional-chain
|
||||
interaction it surfaced. The `#set` write side through `?.` is intentionally left
|
||||
matching real-field behavior (optional-chain assignment unsupported) and is not
|
||||
part of this issue.
|
||||
@@ -400,7 +400,7 @@ impl Platform for AndroidPlatform {
|
||||
}
|
||||
|
||||
poll_events :: (self: *AndroidPlatform) -> []Event {
|
||||
self.events.items.len = 0;
|
||||
self.events.len = 0;
|
||||
sx_android_drain_touches(self, @self.events);
|
||||
result : []Event = ---;
|
||||
result.ptr = self.events.items;
|
||||
|
||||
@@ -144,7 +144,7 @@ impl Platform for SdlPlatform {
|
||||
}
|
||||
|
||||
poll_events :: (self: *SdlPlatform) -> []Event {
|
||||
self.events.items.len = 0;
|
||||
self.events.len = 0;
|
||||
sdl_event : SDL_Event = .none;
|
||||
while SDL_PollEvent(@sdl_event) {
|
||||
if sdl_event == {
|
||||
|
||||
@@ -379,7 +379,7 @@ impl Platform for UIKitPlatform {
|
||||
result : []Event = ---;
|
||||
result.ptr = self.events.items;
|
||||
result.len = self.events.len;
|
||||
self.events.items.len = 0;
|
||||
self.events.len = 0;
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ List :: struct ($T: Type) {
|
||||
|
||||
// No-paren read accessor: `xs.len` → the live element count.
|
||||
len :: (self: *List(T)) -> i64 #get => self.items.len;
|
||||
// Write accessor: `xs.len = n` sets the live count (e.g. `xs.len = 0` to
|
||||
// clear without freeing). Mirrors the `#get` above; the buffer / `cap` are
|
||||
// untouched, so `n` must be `<= cap`.
|
||||
len :: (self: *List(T), v: i64) #set { self.items.len = v; }
|
||||
|
||||
append :: (list: *List(T), item: T, alloc: Allocator = context.allocator) {
|
||||
if list.items.len >= list.cap {
|
||||
|
||||
@@ -647,7 +647,7 @@ remove_timer :: (self: *Scheduler, idx: i64) {
|
||||
self.timers.items[i] = self.timers.items[i + 1];
|
||||
i = i + 1;
|
||||
}
|
||||
self.timers.items.len = self.timers.items.len - 1;
|
||||
self.timers.len = self.timers.len - 1;
|
||||
}
|
||||
|
||||
// Remove a pending sleep timer referencing fiber `f`, if any. A fiber has at
|
||||
@@ -676,7 +676,7 @@ remove_io_waiter :: (self: *Scheduler, idx: i64) {
|
||||
self.io_waiters.items[i] = self.io_waiters.items[i + 1];
|
||||
i = i + 1;
|
||||
}
|
||||
self.io_waiters.items.len = self.io_waiters.items.len - 1;
|
||||
self.io_waiters.len = self.io_waiters.len - 1;
|
||||
}
|
||||
|
||||
// Remove a pending fd-waiter referencing fiber `f`, if any. A fiber has at most
|
||||
|
||||
@@ -575,7 +575,7 @@ GlyphCache :: struct {
|
||||
return; // shaped_buf already has the result
|
||||
}
|
||||
|
||||
self.shaped_buf.items.len = 0;
|
||||
self.shaped_buf.len = 0;
|
||||
if text.len == 0 { return; }
|
||||
|
||||
if is_ascii(text) {
|
||||
|
||||
@@ -141,7 +141,7 @@ UIPipeline :: struct {
|
||||
|
||||
// Reset render_tree nodes (backing is stale after arena reset)
|
||||
self.render_tree.nodes.items = null;
|
||||
self.render_tree.nodes.items.len = 0;
|
||||
self.render_tree.nodes.len = 0;
|
||||
self.render_tree.nodes.cap = 0;
|
||||
|
||||
push Context.{ allocator = xx build_arena, data = context.data } {
|
||||
|
||||
@@ -47,7 +47,7 @@ RenderTree :: struct {
|
||||
}
|
||||
|
||||
clear :: (self: *RenderTree) {
|
||||
self.nodes.items.len = 0;
|
||||
self.nodes.len = 0;
|
||||
self.generation += 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -313,6 +313,15 @@ List :: struct ($T: Type) {
|
||||
// `#get` property accessor: read via no-paren field syntax (`xs.len`),
|
||||
// not `xs.len()`. Takes only `self`; a real field of the same name wins.
|
||||
len :: (self: *List(T)) -> i64 #get => self.items.len;
|
||||
|
||||
// `#set` property accessor: the write counterpart, invoked via field-assign
|
||||
// (`xs.len = n`) rather than `xs.len(n)`. Takes `self` + one value param and
|
||||
// returns void. A property may have BOTH a `#get` and `#set` of the same
|
||||
// name (reads pick the getter, writes the setter); compound assignment
|
||||
// (`xs.len += 1`) reads via `#get` then writes via `#set`. Writing a
|
||||
// `#get`-only property is a "read-only" compile error; a real field of the
|
||||
// same name still wins.
|
||||
len :: (self: *List(T), v: i64) #set { self.items.len = v; }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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