diff options
| author | 2026-10-15 14:36:10 +0330 | |
|---|---|---|
| committer | 2026-10-15 14:36:10 +0330 | |
| commit | 24a21f7520ce91681e573833b7f33156e308964a (patch) | |
| tree | 9b2684173ee515649f4209192d9ed7f4acda6754 | |
| parent | lsp: hold and shared hovers, completion of the keywords, shared variables mar... (diff) | |
docs: hold blocks in LEARN.md (36-hold), spec 5.16, ACTOR.md; TODO: m:n scheduler and follow-ups
| -rw-r--r-- | TODO.md | 4 | ||||
| -rw-r--r-- | docs/ACTOR.md | 2 | ||||
| -rw-r--r-- | docs/LEARN.md | 41 | ||||
| -rw-r--r-- | docs/SPEC.md | 26 | ||||
| -rw-r--r-- | examples/learn/36-hold.out | 1 | ||||
| -rw-r--r-- | examples/learn/36-hold.zc | 24 |
6 files changed, 97 insertions, 1 deletions
@@ -578,3 +578,7 @@ brief someone without re-reading the code. - [x] c typedefs. - [x] the compiler split into phase files listed in `src/compiler/parts`. - [x] standard-library api documentation, `examples/std_tour.zc`. +- [ ] actors, next step: an m:n scheduler. workers still get one os thread each (a 4 MB stack reservation, a kernel thread, its tls), which bounds the count at thousands. run actors as tasks on a few threads, switching at the safepoints the vm already has (statement boundaries, blocking channel reads), with a small per-actor vm stack segment in place of the thread. +- [ ] actors: a worker's compiler tables (rpool, pg_node, dep_node, mapped on first use) stay resident after its first jit compile, about 0.4 MB per busy worker; madvise them away after each compile, or share one set per thread pool once workers are tasks. +- [ ] actors: the snapshot is taken at the first spawn and retaken when the parent drifts past a quarter of its pages; a parent that keeps changing a large heap between spawns pays a page compare of its live memory per spawn (about 2 ms per 18 MB). +- [ ] hold blocks: the arm64 jit is untested here (no cross toolchain on the box); the blocks use only the syscall gate, which every backend already handles. diff --git a/docs/ACTOR.md b/docs/ACTOR.md index 2dc6158..629de20 100644 --- a/docs/ACTOR.md +++ b/docs/ACTOR.md @@ -4,6 +4,8 @@ actors run asynchronously in separate operating-system processes on the native and portable micro interpreters on posix hosts. each worker has its own vm stack, heap, globals and captured handler state. workers can execute in parallel. these are isolated worker processes, rather than shared-memory threads. +a worker starts from a copy-on-write snapshot of its parent's vm: the code, the compiler and every page the parent hasn't changed since the snapshot are one set of physical pages for all workers, and an idle worker costs well under a megabyte. globals are still each worker's own. state every actor sees is declared `shared` in a top-level `hold` block, whose functions use it under the block's lock (spec 5.16, LEARN.md "hold blocks"). + ```zc namespace; include rt, actor; diff --git a/docs/LEARN.md b/docs/LEARN.md index 35ee914..eae9ad4 100644 --- a/docs/LEARN.md +++ b/docs/LEARN.md @@ -1003,6 +1003,47 @@ output: 16640 ``` +## hold blocks + +actors share nothing by default: each one runs on its own copy of the program's globals and its own heap, so a global an actor changes is changed only for that actor. state that every actor sees is declared `shared` inside a top-level `hold` block, a monitor. only the functions written in the block can use its shared variables, and each call to one of them holds the block's lock for the whole call, so everything such a function does is atomic. a call from one of the block's functions to another doesn't lock again. + +using a shared variable anywhere else is an error, and so is using it in a closure, which could run after the lock is released. shared variables hold numbers or arrays of numbers; a pointer would point into one actor's heap. when a program has several hold blocks, a function of one block may call into a block declared after it, but not back into an earlier one while it holds a later one: that order rules out deadlocks between blocks, and breaking it stops the program. + +[example source](../examples/learn/36-hold.zc): + +```zc +namespace; +include rt, actor; +typecheck on; + +// one counter for every actor. only the functions written in the block can touch it, +// and each call holds the block's lock, so a read and the write after it can't interleave. +hold { + shared let hits = 0; + fn hit() { let seen = hits; hits = seen + 1; return seen; } + fn total() { return hits; } +} + +int work(int n) { let i = 0; while (i < n) { hit(); i++; } return n; } + +fn main() { + actor.Actor<int,int>* a = rt.new(actor.Actor<int,int>), b = rt.new(actor.Actor<int,int>); + a.spawn(work); b.spawn(work); + a.send(1000); b.send(1000); + a.recv(fn(int n) { return 0; }); b.recv(fn(int n) { return 0; }); + a.await(); b.await(); + rt.put_n(total()); rt.put_c(10); + a.close(); b.close(); + return 0; +} +``` + +output: + +```text +2000 +``` + ## the optional typechecker `typecheck on;` enables type checking for the declarations that follow; `typecheck off;` disables it. errors report the expected and actual types. the lifetime analyzer runs independently of this switch. a literal 0 can be a null pointer, `word` matches any type, and arrays can decay to pointers. diff --git a/docs/SPEC.md b/docs/SPEC.md index b2aa5ea..a4f8397 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -675,7 +675,7 @@ int y = 7; asm { y @ 1 + y ! } // y is 8 ### 5.1 items -5.1.1 a unit is a sequence of items. an item is: a function definition or prototype, a global variable, `const`, `struct`, `type`, `enum`, `include`, `embed`, `namespace`, `typecheck`, `docs`, `lines`, `memdebug`, `hints`, `enhance`, `extern`, `unchecked;`, `unbounded;`, or a use of a top-level parse word (7.2). +5.1.1 a unit is a sequence of items. an item is: a function definition or prototype, a global variable, `const`, `struct`, `type`, `enum`, `include`, `embed`, `namespace`, `typecheck`, `docs`, `lines`, `memdebug`, `hints`, `enhance`, `extern`, `unchecked;`, `unbounded;`, a `hold` block (5.16), or a use of a top-level parse word (7.2). 5.1.2 `struct`, `type`, `typecheck`, `docs`, `lines`, `namespace`, `enhance` and `extern` are valid only at the top level. `enum`, `embed`, `memdebug`, `hints` and `const` also appear as statements in functions. `include` is top-level only (5.12.7). @@ -811,6 +811,30 @@ rt.put_n(util.twice(1)); // error: module access requires `include util;` 5.15.3 the comment `//:lines NAME` names the source after it NAME, counts its lines from the next line, and turns lines on from the next top-level item. it starts a new file: from the same item, `unbounded;` and `unchecked;` of the source before it no longer apply. +### 5.16 hold blocks + +5.16.1 form: +``` +hold { + shared let c = 0; // shared T name [= constant], shared T[n] name, shared let a, b; + fn a() { ... } // any function definitions or prototypes + int b(int k) { ... } +} +``` +`hold` followed by `{` at the top level starts a hold block; elsewhere `hold` and `shared` are ordinary names. a hold block contains shared variable declarations, functions, and any other top-level items. hold blocks don't nest (error: hold blocks don't nest). each block has a rank, its position among the unit's hold blocks, from 1. + +5.16.2 a shared variable is a global with one value for the whole program, seen by every actor (docs/ACTOR.md); every other global is each actor's own copy. its type is a number type or a fixed array of one. a pointer, string, struct, growable array or function type is error: x: is shared, so it holds numbers: no pointers, strings or structs (each actor has its own heap). an initializer is a constant, as for any global. + +5.16.3 only the functions written in the block can name its shared variables. a use anywhere else, including in another hold block, is error: x: is shared in a hold block; only the functions written inside that block can use it (they hold its lock). a use in a closure, even one written inside the block, is error: x: is shared; a closure can't use it, because it may run after the hold block's lock is released. a function of a hold block can't be `o3` (error: f: is in a hold block and can't be o3). + +5.16.4 a call to a function of a hold block holds the block's lock from entry to return. the lock is reentrant: a call from a function of the block to another of the same block (directly or through other functions) doesn't take it again. inside the call the shared variables behave as ordinary globals; the values another actor wrote are in them on entry, and what the call writes is seen by the next holder after it returns. a read followed by a write (`c = c + 1`) is therefore atomic. + +5.16.5 blocks are taken in rank order: while holding block j, a thread may enter a block k > j, but entering a block k < j stops the program with `zy: hold block k was entered while block j was held; hold blocks are entered in the order they are declared` (exit status 2, a trap). this rules out lock-order deadlocks between blocks. + +5.16.6 an actor that ends (or is closed) while inside a block gives the lock up without writing its changes back. the first entry to a block anywhere in the program takes the shared variables' initial values from the image. + +5.16.7 the implementation keeps the shared values in the host, outside every vm's memory: entering a block from outside (ops 57 and 58 of the syscall gate, at the function's entry and exit) copies them in and back, so every backend runs the block's functions unchanged. a vm without threads (the browser, bare metal) has one machine and needs no lock. + ## 6. ownership and lifetimes this section describes the analyzer: a pass that runs inside the compiler on every function (including code compiled at run time by `run.eval` and friends), tracks heap objects through local variables, inserts frees, and reports violations. it can be switched off (6.12). what it proves is limited to what is stated here. diff --git a/examples/learn/36-hold.out b/examples/learn/36-hold.out new file mode 100644 index 0000000..8bd1af1 --- /dev/null +++ b/examples/learn/36-hold.out @@ -0,0 +1 @@ +2000 diff --git a/examples/learn/36-hold.zc b/examples/learn/36-hold.zc new file mode 100644 index 0000000..6052129 --- /dev/null +++ b/examples/learn/36-hold.zc @@ -0,0 +1,24 @@ +namespace; +include rt, actor; +typecheck on; + +// one counter for every actor. only the functions written in the block can touch it, +// and each call holds the block's lock, so a read and the write after it can't interleave. +hold { + shared let hits = 0; + fn hit() { let seen = hits; hits = seen + 1; return seen; } + fn total() { return hits; } +} + +int work(int n) { let i = 0; while (i < n) { hit(); i++; } return n; } + +fn main() { + actor.Actor<int,int>* a = rt.new(actor.Actor<int,int>), b = rt.new(actor.Actor<int,int>); + a.spawn(work); b.spawn(work); + a.send(1000); b.send(1000); + a.recv(fn(int n) { return 0; }); b.recv(fn(int n) { return 0; }); + a.await(); b.await(); + rt.put_n(total()); rt.put_c(10); + a.close(); b.close(); + return 0; +} |
