Concurrency
Threads, tasks, channels, locks, worker pools and atomics.
Generated by
bin/build_library_doc.pyfrom the kernel sources. Do not edit by hand: change the generator, or the doc comments inkernel/src/, and re-run it.
Tasks and threads
CLASS Task
User-visible handle for a CONCURRENT-spawned task.
────────────────────────────────────────────────────────────────── USER-FACING API ──────────────────────────────────────────────────────────────────
Task is the per-task handle carried as the currentTask reference
inside a CONCURRENT block. User code can only obtain a Task through
the compiler-injected currentTask identifier — Tasks cannot be
stored in fields, assigned to variables, or passed as arguments
(analyzer-enforced; see concurrent_scope_diagnostics.py rules R1–
R5, with R6 rejecting bare CREATE Task(...)).
- currentTask->name() — assigned task name
- currentTask->isCancelled() — cooperative cancellation flag
- currentTask->sleep(int64 millis) — sleep the calling OS thread
────────────────────────────────────────────────────────────────── NOT USER-CONSTRUCTIBLE ──────────────────────────────────────────────────────────────────
Task has no public INIT. The only constructor is INTERNAL and is
invoked by the CONCURRENT-block lowering (_ev_task_scope::spawn
in EV_task_native.hpp): one Task per spawned child, installed
as the new OS thread's thread_local _ev_current_task slot before
the body runs and cleared on body exit.
────────────────────────────────────────────────────────────────── CANCELLATION ──────────────────────────────────────────────────────────────────
Cancellation is cooperative (Decision #2, threads-research §6.8).
The enclosing CONCURRENT scope calls task->cancel() (INTERNAL) on
every spawned Task whenever any sibling throws or the parent scope
cancels. Body code polls currentTask->isCancelled() in long-
running loops and returns at its own pace. There is no async tear-
down at V1; the _ev_task_scope destructor still joins each spawned
OS thread before unwinding.
────────────────────────────────────────────────────────────────── LIFETIME ──────────────────────────────────────────────────────────────────
The Task instance is heap-allocated by _ev_task_scope::spawn and
lives until the enclosing scope's destructor runs. Task does NOT
own its RawThread — the scope guard does, so the join order is
driven by scope unwinding, not by Task CLEANUP.
kernel/src/Task.ev:62
Constructors
INIT(REFERENCE String name)
Methods
METHOD name() RETURNS String
PUBLIC API
METHOD isCancelled() RETURNS boolean
Returns TRUE once the enclosing CONCURRENT scope has flipped this task's cancellation flag. Body code is expected to call this in any long-running loop and exit cooperatively.
METHOD sleep(int64 millis) RETURNS void
Sleep the calling OS thread for millis milliseconds. Lowers
to FOREIGN::ev_thread_sleep_ms(millis) → POSIX nanosleep in
EV_unsafe_concurrency_native.cpp.
The receiver MUST be the current task: the analyzer rules
against storing/passing Task handles ensure currentTask is
the only reachable receiver. sleep does NOT observe the
cancellation flag — callers that want cancellable sleep check
isCancelled() before and after the call.
CLASS Future
Eventually-ready cell holding a worker's result.
────────────────────────────────────────────────────────────────── USER-FACING API ──────────────────────────────────────────────────────────────────
Future[T] is the handle returned by WorkerPool[J, R].submit(...)
. The pool spawns a Task that runs the supplied
function on the job; when the function returns, the kernel calls
future->resolve(result). The submitter holds the Future and
reads the result by calling future->await().
- future->isReady() — non-blocking: TRUE once resolved or failed
- future->await() — pipe-XOR: blocks until ready, then returns either the value (SUCCESS) or a FAILURE STATUS (worker threw, or enclosing CONCURRENT scope cancelled before the value was produced)
────────────────────────────────────────────────────────────────── NOT USER-CONSTRUCTIBLE ──────────────────────────────────────────────────────────────────
Future has no user-callable constructor. The only legitimate
construction site is the kernel: WorkerPool.submit allocates one
Future per submitted job, hands it to the worker Task, and returns
it to the submitter. The analyzer rejects bare CREATE Future[T](...)
in user .ev source (concurrent_scope_diagnostics.py R7, H.2.3
follow-up); the kernel construction path bypasses the analyzer via
emit-time new ENVZN::Future<T>(...) and isn't constrained by R7.
────────────────────────────────────────────────────────────────── CROSS-THREAD SAFETY ──────────────────────────────────────────────────────────────────
Future is one of the kernel-managed exceptions to Envzn's "user code cannot share instances across threads" rule (sibling to Mutex / Channel / Broker). The worker thread mutates the Future via resolve() / fail(); the submitter thread reads via isReady() / await(). All access goes through the internal Mutex, which provides the happens- before ordering, and the partner-locked ThreadCondition handles the blocking wait without busy-polling.
The kernel guarantees exactly one resolve() OR fail() call per Future — the WorkerPool / scope-guard pairs the construction with a single completion path. The Future itself doesn't enforce this; a second resolve() silently overwrites (harmless since notifyAll already fired on the first call). T-aliasing across threads is avoided because resolve() takes T by value (move at the C++ level) and await() returns a fresh clone (see "Value semantics" below).
────────────────────────────────────────────────────────────────── VALUE SEMANTICS ──────────────────────────────────────────────────────────────────
Future[T] retains the resolved value across multiple await() calls.
For class-T, await() returns a .value->clone() so the submitter
thread never aliases the worker's payload. For primitive T (int32
etc.), the := value-copy is sufficient and the WHEN T IMPLEMENTS
Cloneable split picks the ELSE branch automatically.
Per Constitution §12 paragraph 5: a resolved Future retains its
value if the enclosing CONCURRENT scope is later cancelled — only
unresolved Futures resolve to FAILURE("scope cancelled") on
cancellation. The scope guard arranges this by calling fail() only
on Futures whose isResolved field is still FALSE at cancel time.
────────────────────────────────────────────────────────────────── STATUS ──────────────────────────────────────────────────────────────────
Mutex + ThreadCondition substrate via the existing UNSAFE.MUTEX /
UNSAFE.CONDVAR FFI; no new _cxx* reserved types.
kernel/src/Future.ev:86
Constructors
INIT()
Methods
METHOD isReady() RETURNS boolean
PUBLIC API
Non-blocking check. Returns TRUE if the Future has been
resolved (with a value) OR failed (with a STATUS reason);
FALSE while the worker is still in flight. Takes the mutex
briefly to avoid a torn read of .isResolved.
MODIFY METHOD await() RETURNS | T
Blocking await. Returns the resolved value on success; returns a FAILURE STATUS if the worker threw or the enclosing CONCURRENT scope cancelled before resolution.
Pipe-XOR per Constitution §13.1: the caller MATCHes on the
return to dispatch the SUCCESS / FAILURE branches. Spurious
wakeups from pthread are tolerated by the WHILE-not-IF loop
around readyCond->wait().
Multiple await() calls on the same Future are safe — the value is preserved (cloned out for class T, value-copied for primitive T) and subsequent awaits return the same result without re-blocking.
Communication
CLASS Channel
IMPLEMENTS Shareable
Typed, bounded, point-to-point message channel.
────────────────────────────────────────────────────────────────── USER-FACING API ──────────────────────────────────────────────────────────────────
Channel[T] is the blessed primitive for point-to-point handoff of
typed values between CONCURRENT-spawned tasks. It is bounded by
default — the constructor demands an explicit capacity and a
BackpressurePolicy, so "what happens when the consumer falls behind"
is a decision the author makes, never a silent unbounded-growth
default.
Channel[int32] work := CREATE Channel[int32](4, BackpressurePolicy.BLOCK)
CONCURRENT {
PARALLEL { // producer
work->send(1)
work->send(2)
work->close()
}
PARALLEL { // consumer
WHILE work->receive() DO {
printline($RETURNED INTO String)
}
}
}
- send(value) — enqueue; pipe-XOR STATUS. Behaviour at capacity is governed by the BackpressurePolicy (see below).
- receive() — pipe-XOR: blocks until a value is available, then returns it (SUCCESS); returns FAILURE once the channel is closed AND drained.
- close() — sticky shutdown. Wakes every blocked sender and receiver; subsequent send() fails.
- isClosed() — non-blocking state read.
────────────────────────────────────────────────────────────────── BACKPRESSURE ──────────────────────────────────────────────────────────────────
When the buffer is at capacity, send() consults the policy fixed at construction:
- BLOCK — sender blocks until a receiver frees a slot (or the channel closes). The safe default.
- DROP_NEWEST — the incoming value is discarded; send() returns SUCCESS. The backlog is preserved.
- DROP_OLDEST — the oldest buffered value is dropped to make room for the new one.
- CONFLATE — the buffer collapses to hold only the newest value.
────────────────────────────────────────────────────────────────── CROSS-THREAD SAFETY ──────────────────────────────────────────────────────────────────
Channel[T] is one of the kernel-managed exceptions to Envzn's
"user code cannot share instances across threads" rule (sibling to
Mutex / Broker / Future). Sharing IS the point — a producer task and
a consumer task hold the same Channel and coordinate through it. The
internal Lock serialises every queue mutation and provides the
happens-before ordering; the two partner-locked ThreadConditions
(notEmpty / notFull) handle the blocking waits without busy-polling.
────────────────────────────────────────────────────────────────── CLOSE SEMANTICS ──────────────────────────────────────────────────────────────────
close() is sticky. After close:
- receive() drains any buffered values (still SUCCESS), then returns
FAILURE once empty — so a WHILE ch->receive() DO { … } consumer
loop exits cleanly.
- send() returns FAILURE immediately.
A Channel also closes implicitly when its owning variable goes out of
scope (CLEANUP closes before tearing down the buffer).
────────────────────────────────────────────────────────────────── STATUS ──────────────────────────────────────────────────────────────────
Composes Queue[T] storage + Lock + ThreadCondition substrate. capacity is a count; callers pass a positive value.
kernel/src/Channel.ev:96
Constructors
INIT(int64 capacity, BackpressurePolicy policy)
Methods
MODIFY METHOD send(T value) RETURNS STATUS
PUBLIC API
Enqueue value. Pipe-XOR STATUS. The bare-named (owning) form
consumes the source — the value is moved into the buffer. At
capacity, behaviour follows the BackpressurePolicy. Returns
FAILURE if the channel is closed.
MODIFY METHOD receive() RETURNS | T
Remove and return the front value. Pipe-XOR: SUCCESS populates
value; blocks while the buffer is empty and the channel is
open. Returns FAILURE once the channel is closed AND drained, so
a WHILE ch->receive() DO { … } loop terminates on close.
MODIFY METHOD tryReceive() RETURNS | T
Non-blocking receive. Pipe-XOR: SUCCESS returns the front value if
one is buffered; FAILURE immediately if the buffer is empty (never
blocks). A WHILE ch->tryReceive() DO { … } loop drains whatever
is currently buffered and then exits.
MODIFY METHOD close() RETURNS void
Sticky shutdown. Wakes every blocked sender and receiver so they re-test their predicate and exit. Idempotent.
METHOD shareableKind() RETURNS String
Shareable marker — short type tag for debug printers / logs.
METHOD isClosed() RETURNS boolean
Non-blocking state read. TRUE once close() (or scope-exit CLEANUP) has run.
METHOD size() RETURNS int64
Current buffered count. Snapshot under the lock — advisory only (the value may change the instant the lock is released).
CLASS Broker
Typed broadcast pub/sub.
────────────────────────────────────────────────────────────────── USER-FACING API ──────────────────────────────────────────────────────────────────
Broker[T] fans a published value out to every current subscriber.
One broker per typed message. Each subscriber gets its own buffered
Channel[T], so a slow subscriber is governed by its own
BackpressurePolicy and does not block a fast one.
Broker[Match] broker := CREATE Broker[Match]()
CONCURRENT {
PARALLEL { // a subscriber task
Subscription[Match] sub := broker->subscribe(16, BackpressurePolicy.DROP_OLDEST)
WHILE sub->receive() DO { handle($RETURNED) }
}
PARALLEL { broker->publish(m) } // a publisher task
}
- publish(value) — clone the value out to every live subscriber's channel (fan-out).
- subscribe(capacity, policy) — register a new subscriber; returns a scope-bound Subscription[T].
────────────────────────────────────────────────────────────────── OWNERSHIP / LIFETIME ──────────────────────────────────────────────────────────────────
Each subscriber's Channel is co-owned: the broker holds one strong
(reference-counted) handle in an OwnedList[Channel[T]] (the move-only,
non-Cloneable collection — standard collections reject a non-Cloneable
Channel), and the returned Subscription holds a SHARED MUTABLE
REFERENCE that, because Channel is shared-eligible (§9.1), is a strong
handle too. Unsubscribe is lazy: the Subscription's CLEANUP closes its
channel, and publish skips closed channels. A channel is freed when its
last owner drops — so a Subscription may safely outlive the broker (it
keeps its channel alive and can still drain it), and the broker need not
outlive its subscriptions. This is the ARC guarantee that closes the
prior write-after-free.
The broker is itself shared across tasks by PARALLEL capture (like any
channel/mutex), not by a SHARED MUTABLE REFERENCE field — so it is an
ordinary class, internally synchronized by .lock.
kernel/src/Broker.ev:57
Constructors
INIT()
Methods
MODIFY METHOD publish(REFERENCE T value) RETURNS void
Fan value out to every live subscriber. Each subscriber gets its
own clone (class T) or copy (primitive T); a closed (unsubscribed)
channel is skipped. Walks the owned registry with a MUTABLE
REFERENCE local cursor — ownership grants mutable access, so no
SHARED carve-out is needed on the broker side.
MODIFY METHOD subscribe(int32 capacity, BackpressurePolicy policy) RETURNS Subscription[T]
Register a new subscriber with its own buffered channel. Returns a scope-bound Subscription holding a SHARED MUTABLE REFERENCE to the channel. The Subscription is created from the channel handle BEFORE the handle is moved into the registry — the heap channel object is not moved by the handle transfer, so the reference stays valid.
CLASS Subscription
A scope-bound handle to a Broker subscription.
Returned by Broker[T].subscribe(...). It holds a SHARED MUTABLE
REFERENCE to its Channel[T] — the carve-out (§9.6) that lets the
subscription drive the channel's MODIFY methods (receive/tryReceive),
sound because Channel is a SHARED CLASS monitor. Because Channel
is shared-eligible (§9.1) the reference is a strong (reference-counted)
handle: the subscription co-owns the channel with the broker's registry.
Lifecycle: scope-bound. When the subscription's declaring scope ends, CLEANUP closes the channel; the broker skips closed channels on future publishes (lazy unsubscribe). The channel is freed when its last owner drops — so a subscription may outlive the broker and still drain its (closed) channel safely; there is no dangling reference.
kernel/src/Subscription.ev:27
Fields
MUTABLE REFERENCE Channel[T] channelRef
Constructors
INIT(MUTABLE REFERENCE Channel[T] channel)
Methods
MODIFY METHOD receive() RETURNS | T
Blocking receive of the next published value. Pipe-XOR: SUCCESS yields the value; FAILURE once the channel is closed and drained.
MODIFY METHOD tryReceive() RETURNS | T
Non-blocking receive. Pipe-XOR: SUCCESS if a value is buffered;
FAILURE immediately otherwise. A WHILE sub->tryReceive() DO { … }
loop drains whatever is currently buffered and exits.
Synchronisation
CLASS Mutex
kernel/src/Mutex.ev:80
Fields
T data
Constructors
INIT(REFERENCE T initial)
Methods
MODIFY METHOD acquire() RETURNS void
Block until the lock is acquired. Mirrors Lock.acquire().
Use through SYNCHRONIZED in user code — manual acquire is
the C-mutex anti-pattern (forget release, leak the lock).
MODIFY METHOD release() RETURNS void
Release the lock. Mirrors Lock.release(). Pairs with a
previous acquire() on the same Mutex from the same thread.
MODIFY METHOD tryAcquire() RETURNS boolean
Non-blocking attempt to acquire. Mirrors Lock.tryAcquire().
Returns TRUE if the caller now holds the lock (and is
responsible for calling release() exactly once); FALSE if
another thread currently holds it.
CLASS Lock
Kernel mutual-exclusion primitive.
────────────────────────────────────────────────────────────────── PURE SYNCHRONIZATION ──────────────────────────────────────────────────────────────────
Lock is the bare OS-level mutual-exclusion primitive — a thin
Envzn wrapper over UNSAFE.MUTEX + pthread shims. It synchronises
access; it does not bundle protected data. The user-facing
Mutex[T] (see Mutex.ev) composes a Lock with a typed data
field for the "lock + protected state" pattern; kernel-internal
sites that need pure synchronisation (Channel's buffer lock,
Future.value, ThreadCondition partner) hold a Lock directly.
────────────────────────────────────────────────────────────────── USAGE ──────────────────────────────────────────────────────────────────
Use through the SYNCHRONIZED block. The block guarantees the lock is released when control leaves the block — even via RETURN, BREAK, or PANIC from inside:
SYNCHRONIZED .myLock {
// critical section
IF .someCondition THEN {
RETURN // lock still released — guaranteed by block scope
}
.field = newValue
}
The compiler emits an ENVZN::_ev_lock_guard (defined in
EV_unsafe_concurrency_native.hpp) over the Lock's .native
handle; release happens in the guard's destructor on every exit
path.
────────────────────────────────────────────────────────────────── RAW ACQUIRE / RELEASE — narrow uses only ──────────────────────────────────────────────────────────────────
The acquire() / release() / tryAcquire() methods are exposed for the rare cases where the lock-and-unlock paths can't be expressed as a single scope:
- ThreadCondition.wait() / waitFor() take ownership of the lock internally for the sleep window, then re-acquire it before returning.
- tryAcquire() for non-blocking optimistic locks, where the caller wants to bail out if contention is high rather than wait.
Outside those cases: prefer SYNCHRONIZED. Manual acquire/release is the C-mutex anti-pattern (forget release, leak the lock).
kernel/src/Lock.ev:70
Constructors
INIT()
Methods
MODIFY METHOD acquire() RETURNS void
Block until the lock is acquired. Used directly by ThreadCondition (and similar primitives that need explicit acquire/release pairing); everywhere else, prefer the SYNCHRONIZED block which guarantees release on every exit path.
MODIFY METHOD release() RETURNS void
Release the lock. Pairs with a previous acquire() on the same Lock from the same thread. Calling release() without first holding the lock is undefined behaviour at the OS level — pthread does not require the implementation to detect or report this misuse.
MODIFY METHOD tryAcquire() RETURNS boolean
Non-blocking attempt to acquire the lock. Returns TRUE if the caller now holds the lock (and is responsible for calling release() exactly once); FALSE if another thread currently holds it. Useful for optimistic-locking patterns where the caller would rather bail than wait.
Worker pools
CLASS WorkerPool
V1 Part H.4.1: a pool of worker threads that run submitted jobs.
────────────────────────────────────────────────────────────────── USER-FACING API ──────────────────────────────────────────────────────────────────
A WorkerPool owns a fixed set of worker threads that drain a shared,
bounded job queue. A job is any TaskStarter — a self-contained unit of
work whose start() runs on a worker thread. Jobs are moved in (the pool
takes ownership), so a job carries its inputs as value fields and reports
results by writing to an output destination it holds — typically a
SHARED MUTABLE REFERENCE Channel[R] the caller owns and drains. The
pool is non-parametric: it is polymorphic over TaskStarter, not
WorkerPool[T].
WorkerPool pool := CREATE WorkerPool(4, 64, BackpressurePolicy.BLOCK)
pool->submit(CREATE RenderJob(tile, resultChannel))
pool->submit(CREATE RenderJob(tile2, resultChannel))
pool->awaitIdle() // block until both jobs have finished
pool->shutdown() // stop the workers (also runs at scope exit)
- submit(TaskStarter job) — enqueue a job; pipe-XOR STATUS. At queue capacity, behaviour follows the BackpressurePolicy. Fire-and-forget: the job's own STATUS is not surfaced here (a job reports results through its held output channel).
- awaitIdle() — block until the queue is empty AND no job is running. Does not stop the pool.
- shutdown() — sticky stop: drains queued jobs, then joins every worker. Idempotent; also runs from CLEANUP.
────────────────────────────────────────────────────────────────── STRUCTURE ──────────────────────────────────────────────────────────────────
The pool owns a WorkerPoolCore (the thread-safe engine + job queue) and
one shared WorkerBody (the drain loop), and spawns N OS threads via
RawThread. Each thread runs the shared body, which loops on the core.
The threads are OS-detached at spawn; clean shutdown is driven by the
core's live-worker count and exitCond, not by joining std::thread
handles — so the pool keeps no RawThread handles after spawning.
The body holds a SHARED MUTABLE REFERENCE to the core (not to the pool), which is why the engine is a separate owned sub-object: the pool hands out a reference to something it owns, never to itself.
────────────────────────────────────────────────────────────────── V1 SCOPE (DETACHED + LONG_LIVED) ──────────────────────────────────────────────────────────────────
- Attachment is fixed to DETACHED and lifetime to LONG_LIVED — these are reserved concurrency keywords (siblings of DETACHED), not options, so V1 takes no attachment/lifetime argument. ATTACHED / TEMPORARY are reserved for the eventual modifier grammar.
- Worker count is an explicit constructor argument (a cores-default awaits a CPU-count primitive).
- submit() is thread-safe (it forwards into the SHARED CLASS core); pool management — construction and shutdown() — is owner-thread work.
- Tail-append in the core's queue is O(queue-depth); fine for a bounded pool. A constant-time tail or a chunked ring is a perf backlog item.
STATUS: V1 Part H.4.1 (2026-05-29). First real client of RawThread.
kernel/src/WorkerPool.ev:75
Constructors
INIT(int32 workerCount, int32 capacity, BackpressurePolicy policy)
Methods
MODIFY METHOD submit(TaskStarter job) RETURNS STATUS
Enqueue a job for a worker to run. Pipe-XOR STATUS. The bare-named (owning) form consumes the source — the pool takes ownership of the job. At queue capacity, behaviour follows the BackpressurePolicy fixed at construction. Returns FAILURE if the pool has been shut down.
MODIFY METHOD awaitIdle() RETURNS void
Block until the pool is idle — the queue is empty and no job is running. Does not stop the pool; the workers stay alive for more work. Useful as a batch barrier between submit waves.
MODIFY METHOD shutdown() RETURNS void
Sticky shutdown. Queued-but-unstarted jobs still drain; once the queue empties, every worker leaves its loop and this returns. Idempotent — a second call is a no-op.
METHOD size() RETURNS int64
Number of worker threads in this pool.
CLASS WorkerPoolCore
IMPLEMENTS Shareable
WorkerPoolCore is the thread-safe heart of a WorkerPool: a bounded,
move-only job queue plus the synchronisation that lets N worker threads
drain it concurrently while submitters hand work in. It is a
SHARED CLASS (monitor) — the carve-out (§9.6) that lets the worker
threads and the submitting thread drive its MODIFY methods through
shared references without owning it.
The public WorkerPool façade owns one WorkerPoolCore and hands a
SHARED MUTABLE REFERENCE to it to the worker bodies. Splitting the
engine out of the façade is what avoids a self-reference: the façade
passes a reference to an owned sub-object (the same idiom Channel uses
to hand its bufferLock to its ThreadConditions), never to itself.
THE JOB QUEUE
Jobs are TaskStarter instances, moved in (never cloned), so the queue
cannot be Channel[T] (which requires T IS Cloneable). It is a
singly-linked chain of WorkerJobNodes behind a head sentinel: submit
appends at the tail, processNext splices off the front (FIFO). Append is
O(n) in queue depth — acceptable for a bounded pool; see the perf note
in WorkerPool.ev.
LIFECYCLE COUNTERS
- count — jobs currently queued (front .. tail).
- active — jobs currently running on a worker (dequeued, not yet
finished).
idleCondfires when count == 0 AND active == 0 — the signal awaitIdle() waits on. - liveWorkers — workers still in their drain loop. Bumped once per
worker at spawn time by registerWorker() (synchronously,
from WorkerPool.INIT, so it reaches N before any
shutdown can race), decremented by workerExited() when a
worker leaves its loop.
exitCondfires when it hits 0 — the signal awaitWorkersExit() waits on for clean teardown.
STATUS: V1 Part H.4.1 (2026-05-29). Composes WorkerJobNode storage + Lock + ThreadCondition substrate. DETACHED + LONG_LIVED behaviour.
kernel/src/WorkerPoolCore.ev:49
Constructors
INIT(int32 capacity, BackpressurePolicy policy)
Methods
MODIFY METHOD registerWorker() RETURNS void
Register a worker about to be spawned. Called synchronously from WorkerPool.INIT, once per worker, BEFORE any shutdown can be requested — so liveWorkers is exactly the worker count before the first awaitWorkersExit() can observe it.
MODIFY METHOD workerExited() RETURNS void
A worker has left its drain loop. Decrements liveWorkers and, when the last worker exits, wakes awaitWorkersExit().
MODIFY METHOD submit(TaskStarter job) RETURNS STATUS
Submit a job. Pipe-XOR STATUS. The bare-named (owning) form consumes the source — the job is moved into the queue. At capacity, behaviour follows the BackpressurePolicy. Returns FAILURE if the pool is shut down.
MODIFY METHOD processNext() RETURNS boolean
Run one job, blocking until work is available. Returns TRUE after running a job (the caller should loop and call again); FALSE once the pool is shut down AND the queue is drained (the worker should leave its loop). The job runs OUTSIDE the lock so other workers can dequeue and submit() does not stall behind a long-running job.
MODIFY METHOD beginShutdown() RETURNS void
Begin a sticky shutdown. Wakes every blocked worker and submitter so they re-test their predicate; queued-but-unstarted jobs still drain (workers exit only once the queue is empty). Idempotent.
MODIFY METHOD awaitWorkersExit() RETURNS void
Block until every worker has left its drain loop. Called by WorkerPool teardown after beginShutdown(), so the worker bodies and this core are guaranteed untouched by any thread before they are destroyed.
MODIFY METHOD awaitIdle() RETURNS void
Block until the pool is idle — no queued jobs and none running. Does NOT shut the pool down; the workers stay alive for more work.
METHOD shareableKind() RETURNS String
Shareable marker — short type tag for debug printers / logs.
Atomics
CLASS AtomicBoolean
INTERNAL kernel class wrapping a single lock-free atomic boolean.
Replaces the parametric Atomic[boolean]
instantiation. Brian's choice to ship a concrete-types-per-primitive
atomic family (mirroring Rust's core::sync::atomic::AtomicBoolean +
siblings) sidesteps the parametric FOREIGN BIND machinery and keeps
the C-target migration path open (each AtomicX has a single concrete
C11 _Atomic lowering, no templates).
Storage: UNSAFE.HANDLE native — a heap-allocated _Atomic bool
owned across the FFI boundary. Lock-free on every platform Envzn
supports (bool is the simplest lock-free atomic type).
Surface (all three lock-free patterns): - load() single-step indivisible read - store(v) single-step indivisible write - compareExchange(expected, desired) test-and-set; returns TRUE on successful swap
KERNEL-ONLY (INTERNAL). User code reaches atomics through higher- level patterns (Switchboard, Channel, future Future[T] / Pool / etc.) — never directly.
kernel/src/AtomicBoolean.ev:42
Constructors
INIT(boolean initialValue)
Methods
METHOD load() RETURNS boolean
METHOD store(boolean newValue) RETURNS void
METHOD compareExchange(boolean expected, boolean desired) RETURNS boolean
CLASS AtomicBinary
INTERNAL kernel class wrapping a single lock-free atomic octet. Part of the concrete-types-per-T atomic family. See AtomicBool.ev for the design rationale.
Holds a binary — an opaque octet, NOT a number. AtomicUint8 is the
numeric counterpart; the two are distinct Envzn types over the same
underlying C shim, which is why the C entry points still spell
themselves ev_atomic_byte_*.
kernel/src/AtomicBinary.ev:27
Constructors
INIT(binary initialValue)
Methods
METHOD load() RETURNS binary
METHOD store(binary newValue) RETURNS void
METHOD compareExchange(binary expected, binary desired) RETURNS boolean
CLASS AtomicChar8
INTERNAL kernel class wrapping a single lock-free atomic char8 (UTF-8 code unit). Part of the V1 Part E Phase 8 concrete-types-per-T atomic family. See AtomicBool.ev for the design rationale.
FFI signatures use uint8 (E2090 forbids char8 at the C
boundary); the AtomicChar8 method surface uses char8 and the
conversion is a free bit-cast since char8 ≡ uint8 at storage.
kernel/src/AtomicChar8.ev:27
Constructors
INIT(char8 initialValue)
Methods
METHOD load() RETURNS char8
METHOD store(char8 newValue) RETURNS void
METHOD compareExchange(char8 expected, char8 desired) RETURNS boolean
CLASS AtomicChar16
INTERNAL kernel class wrapping a single lock-free atomic char16 (UTF-16 code unit). Part of the V1 Part E Phase 8 concrete-types-per-T atomic family. See AtomicBool.ev for the design rationale.
FFI signatures use uint16 (E2090 forbids char16 at the C
boundary); the AtomicChar16 method surface uses char16 and the
conversion is a free bit-cast since char16 ≡ uint16 at storage.
kernel/src/AtomicChar16.ev:27
Constructors
INIT(char16 initialValue)
Methods
METHOD load() RETURNS char16
METHOD store(char16 newValue) RETURNS void
METHOD compareExchange(char16 expected, char16 desired) RETURNS boolean
CLASS AtomicChar32
INTERNAL kernel class wrapping a single lock-free atomic char32 (UTF-32 code point). Part of the V1 Part E Phase 8 concrete-types-per-T atomic family. See AtomicBool.ev for the design rationale.
FFI signatures use uint32 (E2090 forbids char32 at the C
boundary); the AtomicChar32 method surface uses char32 and the
conversion is a free bit-cast since char32 ≡ uint32 at storage.
kernel/src/AtomicChar32.ev:27
Constructors
INIT(char32 initialValue)
Methods
METHOD load() RETURNS char32
METHOD store(char32 newValue) RETURNS void
METHOD compareExchange(char32 expected, char32 desired) RETURNS boolean
CLASS AtomicInt8
kernel/src/AtomicInt8.ev:22
Constructors
INIT(int8 initialValue)
Methods
METHOD load() RETURNS int8
METHOD store(int8 newValue) RETURNS void
METHOD compareExchange(int8 expected, int8 desired) RETURNS boolean
CLASS AtomicInt16
kernel/src/AtomicInt16.ev:21
Constructors
INIT(int16 initialValue)
Methods
METHOD load() RETURNS int16
METHOD store(int16 newValue) RETURNS void
METHOD compareExchange(int16 expected, int16 desired) RETURNS boolean
CLASS AtomicInt32
kernel/src/AtomicInt32.ev:21
Constructors
INIT(int32 initialValue)
Methods
METHOD load() RETURNS int32
METHOD store(int32 newValue) RETURNS void
METHOD compareExchange(int32 expected, int32 desired) RETURNS boolean
CLASS AtomicInt64
kernel/src/AtomicInt64.ev:21
Constructors
INIT(int64 initialValue)
Methods
METHOD load() RETURNS int64
METHOD store(int64 newValue) RETURNS void
METHOD compareExchange(int64 expected, int64 desired) RETURNS boolean
CLASS AtomicUint8
INTERNAL kernel class wrapping a single lock-free atomic uint8. Part of the concrete-types-per-T atomic family. See AtomicBool.ev for the design rationale.
The NUMERIC octet. AtomicBinary is the opaque-octet counterpart; the two are distinct Envzn types over the same underlying C shim.
kernel/src/AtomicUint8.ev:25
Constructors
INIT(uint8 initialValue)
Methods
METHOD load() RETURNS uint8
METHOD store(uint8 newValue) RETURNS void
METHOD compareExchange(uint8 expected, uint8 desired) RETURNS boolean
CLASS AtomicUint16
kernel/src/AtomicUint16.ev:21
Constructors
INIT(uint16 initialValue)
Methods
METHOD load() RETURNS uint16
METHOD store(uint16 newValue) RETURNS void
METHOD compareExchange(uint16 expected, uint16 desired) RETURNS boolean
CLASS AtomicUint32
INTERNAL kernel class wrapping a single lock-free atomic uint32. Part of the concrete-types-per-T atomic family. See AtomicBool.ev for the design rationale.
Shares the C ev_atomic_u32_* machinery with AtomicChar (char ≡
uint32 code point) — separate Envzn types, same underlying C shim.
kernel/src/AtomicUint32.ev:25
Constructors
INIT(uint32 initialValue)
Methods
METHOD load() RETURNS uint32
METHOD store(uint32 newValue) RETURNS void
METHOD compareExchange(uint32 expected, uint32 desired) RETURNS boolean
CLASS AtomicUint64
kernel/src/AtomicUint64.ev:21
Constructors
INIT(uint64 initialValue)
Methods
METHOD load() RETURNS uint64
METHOD store(uint64 newValue) RETURNS void
METHOD compareExchange(uint64 expected, uint64 desired) RETURNS boolean
Contracts
GROUP Concurrency
kernel/src/interfaces.ev:141
INTERFACE Shareable
Shareable — the marker interface for a SHARED CLASS monitor: an
interior-mutable, self-synchronizing object that may be aliased by
reference across concurrent tasks. Per Constitution §771, a
SHARED CLASS MUST IMPLEMENTS Shareable (omitting it is E2115),
and Shareable is the marker that admits a type into the two places
the borrow model otherwise forbids aliasing:
- a SHARED MUTABLE REFERENCE field (§9.6), and
- the move-only OwnedList[T] collection (owned and moved, never
cloned — so it needs no Cloneable bound; Shareable is the
explicit constraint atom that admits the non-Cloneable monitor).
The kernel's SHARED CLASS monitors are Channel[T] (the canonical
one) and WorkerPoolCore. These are the only kernel types that
implement Shareable, and the only element type used in an
OwnedList is Channel (the Broker's subscriber registry).
NOT Shareable — the other concurrency types are ordinary classes,
not monitors, and deliberately do NOT implement this marker:
- Broker / Mutex — shared across tasks by PARALLEL capture
plus an internal Lock, not by a SHARED MUTABLE REFERENCE
field, so they need no carve-out.
- Subscription — a scope-bound handle that merely HOLDS a
SHARED MUTABLE REFERENCE Channel; it rides on Channel's
shareability rather than being a monitor itself.
- Future — an ordinary result cell, not aliased mutably.
A type opts in via IMPLEMENTS Shareable. Envzn forbids empty
interfaces (E6001), so the marker carries one descriptive method —
shareableKind() returns a short human-readable tag for the
concrete type (used by debug printers / kernel logs), mirroring the
Inoperative.inoperativeReason() idiom. OwnedList itself never
calls it; the interface exists for the type-bound.
kernel/src/interfaces.ev:213
Methods
METHOD shareableKind() RETURNS String
INTERFACE TaskStarter
TaskStarter — body interface for spawnable threads.
A class that IMPLEMENTS TaskStarter can be passed to
CREATE Thread(...). When the user calls Thread.start() (the outer
kickoff), the runtime spawns an OS thread, sets up that thread's
Channel and current-thread accessors, and then invokes
body->start() (this interface's method) on the new thread. The
dev never calls TaskStarter.start() directly — the symmetric
naming mirrors CREATE → INIT.
Example:
CLASS Worker IMPLEMENTS TaskStarter { PRIVATE int32 jobId INIT(int32 jobId) { .jobId = jobId } METHOD start() RETURNS STATUS { Channel->current()->subscribe("jobs") // ... body runs on the spawned OS thread ... RETURN (SUCCESS) } }
Thread t := CREATE Thread("worker", CREATE Worker(42)) t->start() // outer kickoff — runtime calls Worker.start() inside
V1 ban: CREATE Thread(LAMBDA { ... }) is a parse error (Bug #43); thread bodies must be a TaskStarter class. V2 will add the LAMBDA shortcut as a strict superset (move-semantic captures only, REFERENCE captures rejected); existing V1 TaskStarter code continues to work unchanged.
kernel/src/interfaces.ev:705
Methods
METHOD start() RETURNS STATUS
ENUM BackpressurePolicy
BackpressurePolicy — what a Channel[T] / Broker[T] subscriber does when its bounded buffer is full at send/publish time. V1 Part H (ENVZN_CONSTITUTION.md §12). The Channel constructor and the Broker.subscribe call both take an explicit policy; there is no default — every site declares its choice so the failure mode is visible.
- BLOCK — sender blocks until a slot is available. The safe choice for correctness-critical streams; propagates backpressure upstream.
- DROP_OLDEST — drop the oldest queued value, append the new one. The Kotlin SharedFlow default; friendly to telemetry-style streams where freshness wins.
- DROP_NEWEST — drop the incoming value, keep the buffered backlog. Used when older messages have priority (e.g. ordered event replay).
- CONFLATE — collapse to a single most-recent value; the buffer holds only the latest. State-update streams where intermediate values don't matter.
kernel/src/enums.ev:152
| Case | Description |
|---|---|
? |
— |
? |
— |
? |
— |
? |
— |