diff options
| author | 2026-10-15 02:51:54 +0330 | |
|---|---|---|
| committer | 2026-10-15 02:51:54 +0330 | |
| commit | fc89e5ea115d6da2217b52a87ba8eb73c207e916 (patch) | |
| tree | c5b8e69edd139b8863bb32bcbdecff3bab03ebc5 | |
| parent | std: File, the first user of drops: std.Host.open(path, mode) returns an owne... (diff) | |
actor: a declared actor without its own drop gets one that awaits owed replies and closes the endpoint; the handler reaches the struct through endpoint.state so the struct isn't shared; spawn hint skips mailbox structs
| -rw-r--r-- | docs/ACTOR.md | 2 | ||||
| -rw-r--r-- | docs/SPEC.md | 2 | ||||
| -rw-r--r-- | docs/design/destructors.md | 2 | ||||
| -rw-r--r-- | src/compiler/builtins.zc | 4 | ||||
| -rw-r--r-- | src/lib/actor.zc | 15 | ||||
| -rw-r--r-- | tests/actor/accept/drop_closes.zc | 25 | ||||
| -rw-r--r-- | tests/drivers/actor_test.py | 2 |
7 files changed, 45 insertions, 7 deletions
diff --git a/docs/ACTOR.md b/docs/ACTOR.md index 6ee5627..2dc6158 100644 --- a/docs/ACTOR.md +++ b/docs/ACTOR.md @@ -43,7 +43,7 @@ for a handler function or closure without a declared actor, allocate `actor.Acto messages copy their typed object graphs into the receiving context. supported values include scalars, enums, strings, packed fields, byte strings, growable arrays, pointers to structs, and function values with typed captures. repeated references and cycles preserve identity within a copied graph. integer payloads that happen to equal heap addresses remain integers. changes to a received graph do not modify the sender's graph. ordinary language rules for aggregate parameters still apply. -`await()` requires a registered receiver when messages are pending. `close()` cancels remaining work, closes the channel and joins the worker; repeated closes succeed. later sends and receives return `-1`. close every spawned actor before discarding it. sends may wait for channel space under backpressure, and polling a large result can wait for its remaining bytes. +`await()` requires a registered receiver when messages are pending. `close()` cancels remaining work, closes the channel and joins the worker; repeated closes succeed. later sends and receives return `-1`. an actor struct that declares no `drop` of its own gets one: when its owner's block ends, or it is freed or collected, it waits for the replies a registered receiver is still owed (as `await()` does), then closes. so an actor owned by a block needs no explicit `close()`; one moved out of the block lives on with its new owner. a struct that declares its own `drop` should close the actor there. the worker's snapshot of the struct never closes anything. sends may wait for channel space under backpressure, and polling a large result can wait for its remaining bytes. ## current limits diff --git a/docs/SPEC.md b/docs/SPEC.md index 1b07ae0..7f37e33 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -996,7 +996,7 @@ let guard = drop(heap(0), fn() { unlock(); }); // unl ``` 6.15.2 a value dies, and its drop runs, where 6.5 frees it: at the end of its owner's block or an early exit, at `rt.free`, when its owner is reassigned (6.8.5), or with its container, whose drop runs before its children's. a new value assigned to the owner can have its own drop set on the next statement, like a declaration's. a value with a drop that nothing holds (a temporary) dies at the end of its full statement, with hint: value with drop is never bound; it drops at the end of this statement. an owned local with a drop is not "never used" (6.5.2): holding it is its use. 6.15.3 the collector runs the drop of an unreachable value after its sweep, never during it, and at most once: the value is kept until a later collection finds it unreachable again. a drop that stores `this` somewhere reachable keeps the value alive, and its drop does not run again. among unreachable values, one reached by another waits until that one has dropped (parent first); values that reach each other (a cycle, or a drop that captures its own value) have nothing outside to drop first, so one of them drops per collection, in an unspecified order, each once. there is no collection at exit, so collector-owned values are not dropped then. a freed block forgets its drop, so a block reused at the same address has none. -6.15.4 copying a value with a drop into an actor message is error: x: has drop and can't be copied into a message. `spawn` copies the whole vm into the worker process, so both processes hold an owned value that is live at the call and both run its drop: hint: x: has drop and is live at spawn; the worker's copy drops it too. +6.15.4 copying a value with a drop into an actor message is error: x: has drop and can't be copied into a message. `spawn` copies the whole vm into the worker process, so both processes hold an owned value that is live at the call and both run its drop: hint: x: has drop and is live at spawn; the worker's copy drops it too. the hint is not given for structs declared with `actor`: one that declares no `drop` gets one that waits for the replies its receiver is owed, closes the endpoint, and does nothing in the worker (docs/ACTOR.md). 6.15.5 nothing moves out of a value with a drop, whose drop may need what it holds. a field or element of it is a borrowed projection and can't be given away (error: a borrowed projection cannot transfer ownership; transfer its owner), and `pop` on an array that has a drop, or on an array field of a value with a drop, is error: a: can't move out of a value with drop. there is no `take`. borrowing a child is fine, and an array without a drop hands out its elements with `pop` as before, drops and all. ## 7. compile-time execution diff --git a/docs/design/destructors.md b/docs/design/destructors.md index 7d0c950..c828cbd 100644 --- a/docs/design/destructors.md +++ b/docs/design/destructors.md @@ -180,4 +180,4 @@ untyped `rt.new(n)` blocks take a drop like any other value, so there is no sepa - o3: `rt.o3_enter`/`rt.o3_leave` around `o3 { }` blocks and calls from normal code into o3 functions; `gc` inside sets `gc_pending`, the outermost leave runs it. - compiler: `heap(e)`, the three drop forms (a closure there gets `this`), the struct `drop` method as type drop (`set_tdrop` after `new(T)` and `heap`), set-once and set-early on locals, temporaries held in a hidden slot until the statement ends with a hint, the actor `send` check, the o3 errors. - since: reassigning an `m` owner frees the old value with a hint (6.8.5; `reassign_free` in analyzer.zc, which frees without freevar's clearing store, since a captured local's slot holds its cell); `pop` on an array with a drop, or an array field of one, is refused (6.15.5; fields were already borrowed projections); the spawn hint on `x.spawn()` of a mailbox struct (`spawn_hint`); memreport's `(drop)` marker; `break`/`continue` out of an `o3 { }` block call `o3_leave` for each block they leave (`lpo3`, the depth each loop began at); an o3 receiver method called from normal code enters and leaves the region (`methcall`); a candidate that reaches itself in `gc_drops` (`F_CYCLE`, a cycle or a drop capturing its value) drops now, one per collection. -- not yet: first users (file descriptors in `std.Host`, processes in `actor.zc`). +- first users: `std.File` (`Host.open` returns a `File*.u` whose drop closes the fd; `writefile` is built on it), and every declared actor struct without its own `drop` gets a generated one that waits for the replies its receiver is owed and closes the endpoint (the generated handler reaches the struct through `endpoint.state` rather than capturing `this`, since a captured value is shared and a shared value's free is the collector's; the generated drop returns at once in a worker, whose snapshot owns nothing). the spawn hint skips mailbox structs. diff --git a/src/compiler/builtins.zc b/src/compiler/builtins.zc index fb2dcb2..be8c3b4 100644 --- a/src/compiler/builtins.zc +++ b/src/compiler/builtins.zc @@ -858,11 +858,13 @@ int tdroppable(word t) { int ldroppable(int i) { return (local_dropset && local_dropset[i]) || tdroppable(ltype[i]); } // x.spawn() on a mailbox struct snapshots the whole vm into a worker process, so an // owned value with a drop that is live there is dropped once in each process (6.15.4) +// a pointer to a struct with a mailbox: its drop knows a worker's copy closes nothing +int mailbox_type(word t) { return t && isptr(t) && t[4][0] == TY_STRUCT && tfindfield(t[4], intern("__mailbox")) >= 0; } int spawn_hint() { int i = lbase; if (!fchk || !local_dropset) return 0; while (i < nloc) { - if (!empty(lstate[i]) && !isalias(i) && lstate[i] != S_BORROW && ldroppable(i) && hint_begin(tpack(), HINT_SMALL)) { + if (!empty(lstate[i]) && !isalias(i) && lstate[i] != S_BORROW && ldroppable(i) && !mailbox_type(ltype[i]) && hint_begin(tpack(), HINT_SMALL)) { rt.put_s(lname[i]); rt.put_s(": has drop and is live at spawn; the worker's copy drops it too"); dg_close(); } i++; diff --git a/src/lib/actor.zc b/src/lib/actor.zc index a129f7f..c013441 100644 --- a/src/lib/actor.zc +++ b/src/lib/actor.zc @@ -170,8 +170,10 @@ unchecked int parse_actor() { rt.outgrow = 0; rt.outmode = 2; rt.outbuf = buffer; rt.outn = 0; rt.outcap = 16383; declaration_method(type, "spawn"); rt.put_s(") { if (this.__mailbox) return -1; actor.Actor<"); run.tprint(signature[8]); rt.put_s(","); run.tprint(signature[4]); rt.put_s(">* endpoint = rt.new(actor.Actor<"); - run.tprint(signature[8]); rt.put_s(","); run.tprint(signature[4]); rt.put_s(">); this.__mailbox = endpoint; return endpoint.spawn(fn("); - run.tprint(signature[8]); rt.put_s(" message) { return this.handle(message); }); } "); + run.tprint(signature[8]); rt.put_s(","); run.tprint(signature[4]); rt.put_s(">); this.__mailbox = endpoint; endpoint.state = this as word; return endpoint.spawn(fn("); + // the handler reaches the struct through the endpoint: capturing this would share it, and a + // shared struct is the collector's, so its drop would not run when its owner's block ends + run.tprint(signature[8]); rt.put_s(" message) { return (endpoint.state as "); declaration_type(type); rt.put_s("*).handle(message); }); } "); int index = 0; while (index < 5) { if (index == 0) declaration_method(type, "send"); else if (index == 1) declaration_method(type, "recv"); else if (index == 2) declaration_method(type, "await"); else if (index == 3) declaration_method(type, "close"); else declaration_method(type, "poll"); @@ -182,6 +184,14 @@ unchecked int parse_actor() { if (index == 0) rt.put_s("send(message);"); else if (index == 1) rt.put_s("recv(callback);"); else if (index == 2) rt.put_s("await();"); else if (index == 3) rt.put_s("close();"); else rt.put_s("poll();"); rt.put_s(" } "); index++; } + // the actor's process is a resource: unless the struct declares its own drop, its drop + // waits for the replies a registered receiver is owed, then closes the endpoint (cancelling + // the worker) when its owner's block ends, when it is freed, or when the collector reclaims + // it. a worker's snapshot of the struct closes nothing + if (run.methof(type, run.intern("drop")) < 0) { + declaration_method(type, "drop"); rt.put_s(") { if (actor.worker) return 0; actor.Actor<"); run.tprint(signature[8]); rt.put_s(","); run.tprint(signature[4]); rt.put_s(">* endpoint = this.__mailbox as actor.Actor<"); + run.tprint(signature[8]); rt.put_s(","); run.tprint(signature[4]); rt.put_s(">*; if (endpoint) { endpoint.await(); endpoint.close(); } return 0; } "); + } buffer[rt.outn] = 0; rt.outgrow = previous_grow; rt.outmode = mode; rt.outbuf = output; rt.outn = length; rt.outcap = capacity; word tokens = run.tokens(buffer); if (generic) { @@ -211,6 +221,7 @@ actor Actor<I, O> { O(I) handler; int(O) receiver; int fd, process, pending, closed, dispatcher_index; + word state; // a declared actor struct, for its handler (see parse_actor) // threads receive a private vm snapshot and an owned channel endpoint. unchecked fn spawn(O(I).d callback) { diff --git a/tests/actor/accept/drop_closes.zc b/tests/actor/accept/drop_closes.zc new file mode 100644 index 0000000..08fd02c --- /dev/null +++ b/tests/actor/accept/drop_closes.zc @@ -0,0 +1,25 @@ +// destructors: an actor's drop waits for the replies its receiver is owed, then closes the endpoint, so the worker is gone when the owner's block ends; a struct that declares its own drop keeps it +namespace; +include rt, actor; +actor Echo { int total; int handle(int x) { return x; } } +int closed; +actor Mine { int total; int handle(int x) { return x; } void drop() { closed++; } } +fn main() { + int before = rt.syscall(55, 0, 0, 0); + { + Echo* e = rt.new(Echo); + e.spawn(); + rt.put_n(rt.syscall(55, 0, 0, 0) - before); rt.put_c(32); + } + rt.put_n(rt.syscall(55, 0, 0, 0) - before); rt.put_c(32); + let total = 0; + { + Echo* f = rt.new(Echo); + f.spawn(); f.recv(fn(int n) { total += n; return 0; }); f.send(5); f.send(6); + } + rt.put_n(total); rt.put_c(32); + rt.put_n(rt.syscall(55, 0, 0, 0) - before); rt.put_c(32); + { Mine* m = rt.new(Mine); } + rt.put_n(closed); rt.put_c(10); + return 0; +} diff --git a/tests/drivers/actor_test.py b/tests/drivers/actor_test.py index 929bbf4..e8988c0 100644 --- a/tests/drivers/actor_test.py +++ b/tests/drivers/actor_test.py @@ -11,7 +11,7 @@ parser.add_argument('--case', default='') parser.add_argument('--library', default='') args = parser.parse_args() root = Path(__file__).resolve().parents[2] -expected = {'parallel': '0 0\n11 21\n', 'state': '0 5 7\n4\n', 'graphs': '25 a 5\n', 'results': 'ok\n', 'plain': '3\n', 'module_helpers': '10\n', 'safepoint': '8\n', 'values': '1\n', 'functions': '12\n', 'pointer_scalar': '1\n', 'declaration': '5 7 0 0\n', 'lifecycle': '-1 3\n', 'generic_extensions': '3\n', 'failed_worker': '-1 -1 3\n', 'generic_declaration': '5 7 4 0\n', 'generic_typed_method': '7 2\n'} +expected = {'parallel': '0 0\n11 21\n', 'state': '0 5 7\n4\n', 'graphs': '25 a 5\n', 'results': 'ok\n', 'plain': '3\n', 'module_helpers': '10\n', 'safepoint': '8\n', 'values': '1\n', 'functions': '12\n', 'pointer_scalar': '1\n', 'declaration': '5 7 0 0\n', 'lifecycle': '-1 3\n', 'generic_extensions': '3\n', 'failed_worker': '-1 -1 3\n', 'generic_declaration': '5 7 4 0\n', 'generic_typed_method': '7 2\n', 'drop_closes': '1 0 11 0 1\n'} count = 0 for case in sorted((root / 'tests/actor').glob('*/*.zc')): if args.case and args.case not in str(case): |
