Expand description
§logicaffeine-runtime
The deterministic, replayable concurrency runtime for the Logicaffeine interpreter and VM: the task scheduler, FIFO channels, Select, a logical-clock timer wheel, and the seed/trace machinery that give Logos concurrency its operational semantics. A run is a deterministic function of (program, seed) and replays bit-for-bit from (program, trace).
Part of the Logicaffeine workspace. Tier 0 — deliberately dependency-free (pure std, tokio-free, WASM-safe). Used by the interpreter/VM tiers; NOT linked into AOT binaries (which lower to host primitives via logicaffeine_system).
The crate is a handful of focused modules: scheduler (the M:1 cooperative driver + process_step core), executor (the native M:N work-stealing driver), task (the Task/TaskStep/TaskCtx interface the tiers implement), channel (ChanId + the FIFO/rendezvous/unbounded channel semantics), seed (the Chooser/SeededRng/SchedTrace determinism contract), payload (RtPayload, the Send value that crosses task and OS-thread boundaries), and config (SchedulerConfig/SchedulePolicy/ClockMode).
§Role in the workspace
This crate is the interpreter/VM concurrency model, not the AOT one. By charter it is never linked into AOT-compiled binaries (invariant I6 of work/FINISH_INTERPRETER.md) — the compiled path uses logicaffeine-system’s platform services. Keeping the crate zero-dependency (empty [dependencies], pure std) is what enforces that boundary at the dependency level. Marshalling between interpreter RuntimeValue / VM Value and the wire payload lives in logicaffeine-compile; this crate defines only the wire shape (RtPayload) and the scheduling engine. The user-facing, language-level concurrency story is in docs/concurrency.md.
Determinism contract. Every nondeterministic scheduling decision flows through one choke point, Chooser::decide(kind, options) -> chosen; nothing else draws entropy. In record mode it draws from a seeded SplitMix64 RNG (SeededRng — tiny, allocation-free, identical on native and WASM) and logs a ChoicePoint { kind, options, chosen }; in replay mode it re-issues the recorded choice and asserts the decision shape (kind + option count) still matches, panicking on divergence. The trace is id-agnostic (no TaskId/ChanId), so it never depends on identity allocation. ChoiceKind covers TaskPick, SelectWinner, ChanWaiterWake, TimerTieBreak, WorkerPlacement.
Two drivers, one semantics.
- Cooperative (M:1) —
Scheduler::runmultiplexes everyTaskon a single thread, resolving each decision through the embeddedChooser. This is the only driver underwasm32. A task is a small state machine: eachpolladvances it until it next blocks (channel / select / timer), yields, spawns, or exits; the scheduler parks it and resumes it throughTaskCtx. - Work-stealing (M:N) —
executor::run_workstealing_seededgives genuine multicore to the VM/tree-walker tiers usingstd::thread::scope+std::sync::mpsc(never tokio, never crossbeam). Because a task body holdsRcheaps and is irreducibly!Send, tasks never cross threads — only aSendSpawnDescdoes, and each worker builds its!Sendbody locally from its own program clone. A central coordinator owns the singleScheduler+Chooser; workers only poll bodies in parallel and report aSendstep back. The coordinator applies reports in deterministic pick order through the sameprocess_step, so for a fixed seed the trace and output are byte-identical to the cooperative run. “Spawns are stealable; resumptions are pinned.” This whole path is#[cfg(not(target_arch = "wasm32"))]— a target gate (no OS threads on wasm), not an opt-in Cargo feature.
Determinism holds under every SchedulePolicy: Fifo/Lifo/RoundRobin/Priority are deterministic by construction, and Random resolves its pick through the seeded Chooser.
§Public API
Re-exported from the crate root (see src/lib.rs):
// Entry points — record once, replay forever.
pub fn run_with_seed<'t, F: FnOnce(&mut Scheduler<'t>)>(
config: SchedulerConfig, seed: SchedSeed, setup: F,
) -> (RunOutcome, SchedTrace);
pub fn run_with_trace<'t, F: FnOnce(&mut Scheduler<'t>)>(
config: SchedulerConfig, trace: SchedTrace, setup: F,
) -> RunOutcome;
// The scheduler.
impl<'t> Scheduler<'t> {
pub fn new(config: SchedulerConfig, chooser: Chooser) -> Self;
pub fn spawn(&mut self, task: Box<dyn Task<'t> + 't>) -> TaskId;
pub fn spawn_main(&mut self, task: Box<dyn Task<'t> + 't>) -> TaskId; // its Exit ⇒ Done payload
pub fn new_chan(&mut self, capacity: Option<usize>) -> ChanId; // None=unbounded, Some(0)=rendezvous
pub fn new_default_chan(&mut self) -> ChanId;
pub fn run(&mut self) -> RunOutcome; // Done(payload) | Deadlock
pub fn into_trace(self) -> SchedTrace;
}
// The unit the scheduler multiplexes.
pub trait Task<'t> {
fn poll(&mut self, ctx: &mut TaskCtx) -> TaskStep<'t>;
fn priority(&self) -> u8 { 0 }
fn take_output(&mut self) -> Vec<String> { Vec::new() } // collected per slice for M:N pick-order flush
}
// What a poll asks the scheduler to do next.
pub enum TaskStep<'t> {
Yield, Recv(ChanId), Send(ChanId, RtPayload),
TrySend(ChanId, RtPayload), TryRecv(ChanId),
Select(Vec<SelectArm>), Sleep(u64), NewChan(Option<usize>),
Spawn(Box<dyn Task<'t> + 't>),
SpawnDesc { func: u16, args: Vec<RtPayload>, want_handle: bool }, // Send form for M:N
Await(TaskId), Abort(TaskId), Close(ChanId), Exit(RtPayload),
}
pub enum SelectArm { Recv(ChanId), Timeout(u64) } // u64 = logical ticks
pub struct TaskCtx { resumed_with, selected_arm, new_chan, spawned }
// The determinism contract.
pub enum Chooser { Record { .. }, Replay { .. } }
impl Chooser {
pub fn record(seed: SchedSeed) -> Self;
pub fn replay(trace: SchedTrace) -> Self;
pub fn decide(&mut self, kind: ChoiceKind, options: usize) -> usize;
pub fn into_trace(self) -> SchedTrace;
}
pub struct SchedSeed(pub u64);
pub struct SchedTrace { pub seed: SchedSeed, pub choices: Vec<ChoicePoint> }
pub struct ChoicePoint { pub kind: ChoiceKind, pub options: usize, pub chosen: usize }
pub enum ChoiceKind { TaskPick, SelectWinner, ChanWaiterWake, TimerTieBreak, WorkerPlacement }
pub struct SeededRng { /* SplitMix64 */ }
// Config (fluent setters: with_policy / with_clock / with_channel_capacity / with_workers).
pub struct SchedulerConfig { policy, clock, default_channel_capacity: 32, preempt_every: 10_000, workers: 1 }
pub enum SchedulePolicy { Fifo /*default*/, Lifo, RoundRobin, Random, Priority }
pub enum ClockMode { Logical /*default, virtual time*/, Wall }
pub enum RunOutcome { Done(RtPayload), Deadlock }
pub struct ChanId(pub u64);
pub struct TaskId(pub u64);
// The Send wire value that crosses task (and OS-thread) boundaries.
pub enum RtPayload {
Nothing, Int(i64), BigInt { .. }, Rational { .. }, Float(f64), Bool, Char, Text(String),
List(..), Tuple(..), Set(..), Map(..), Struct { .. }, Inductive { .. },
Duration(i64), Date(i32), Moment(i64), Span { .. }, Time(i64),
Chan(ChanId), TaskHandle(TaskId), Peer(String),
}Native-only (#[cfg(not(target_arch = "wasm32"))]), the work-stealing driver:
pub fn run_workstealing_seeded<'env, B>(
config: SchedulerConfig, seed: SchedSeed, main: SpawnDesc, build: B,
) -> WsOutcome
where B: Fn(SpawnDesc) -> Box<dyn Task<'env> + 'env> + Sync;
pub type FuncIdx = u16;
pub struct SpawnDesc { pub func: FuncIdx, pub args: Vec<RtPayload>, pub priority: u8, pub is_main: bool }
pub struct WsOutcome { pub outcome: RunOutcome, pub output: Vec<String>, pub trace: SchedTrace }The interpreter and VM implement Task over their own continuations; build reconstructs a !Send body from a Send SpawnDesc per worker. Under ClockMode::Logical the timer wheel runs on a virtual clock, so Sleep/After are ordered logically and elapse instantly; same-tick timers are tie-broken through the seeded Chooser, keeping timeout races replayable. A closed channel delivers Nothing on receive instead of blocking and counts as receive-ready for Select.
§Feature flags
| Feature | Default | Gates |
|---|---|---|
cooperative | yes | The M:1 cooperative single-thread/event-loop driver — the only mode under WASM. (Currently a no-op marker: the cooperative scheduler is always compiled.) |
There is no work_stealing feature. The native M:N work-stealing driver (executor) is compiled automatically off-WASM via #[cfg(not(target_arch = "wasm32"))] and excluded on wasm32 (no OS threads). Genuine multicore is a property of the native build, not an opt-in toggle.
§License
Business Source License 1.1 — see LICENSE.md.
Re-exports§
pub use channel::ChanId;pub use config::ClockMode;pub use config::SchedulePolicy;pub use config::SchedulerConfig;pub use payload::RtPayload;pub use scheduler::run_with_seed;pub use scheduler::run_with_trace;pub use scheduler::ChanView;pub use scheduler::RunOutcome;pub use scheduler::SchedSnapshot;pub use scheduler::Scheduler;pub use scheduler::TaskStateKind;pub use scheduler::TaskView;pub use seed::ChoiceKind;pub use seed::ChoicePoint;pub use seed::Chooser;pub use seed::SchedSeed;pub use seed::SchedTrace;pub use seed::SeededRng;pub use task::SelectArm;pub use task::Task;pub use task::TaskCtx;pub use task::TaskId;pub use task::TaskStep;pub use executor::run_workstealing_seeded;pub use executor::FuncIdx;pub use executor::SpawnDesc;pub use executor::WsOutcome;
Modules§
- channel
- Channels — FIFO, bounded, move-semantics pipes (the language’s
Pipe). - config
- Scheduler configuration — annotatable knobs with a nice default.
- executor
- The work-stealing M:N native driver (Phase 7 of work/FINISH_INTERPRETER.md).
- payload
RtPayload— theSend-able value subset that crosses task (and OS-thread) boundaries via channels.- scheduler
- The scheduler core + cooperative driver.
- seed
- Seed, deterministic RNG, and the choice trace — the determinism contract.
- task
- Tasks — the unit the scheduler multiplexes.