Skip to main content

logicaffeine_compile/codegen_c/
concurrency.rs

1//! The **concurrency-lowering knob** for the C-family backends.
2//!
3//! LOGOS's deterministic concurrency (channels / tasks / `Await the first of` / virtual-time `Sleep`)
4//! needs a task to SUSPEND at a blocking op and RESUME later. There are two ways to lower that suspend,
5//! and — like the CUDA [`crate::codegen_cuda::CudaKnobs`] lattice — **neither is strictly better**, so it
6//! is a knob with a documented trade-off ([`ConcurrencyLowering::tradeoffs`]). Unlike most knobs, one end
7//! is **capability-forced**: a target with no `ucontext`/`setjmp` (a GPU kernel, WASM, bare metal) can
8//! ONLY do the stackless form. So the lattice point is chosen by [`ConcurrencyLowering::for_target`]:
9//! the best-general choice each target *can* express.
10//!
11//! - [`ConcurrencyLowering::Ucontext`] — stackful coroutines (`getcontext`/`makecontext`/`swapcontext`).
12//!   A blocking op yields mid-loop / mid-nested-call and the native stack survives, so codegen is SIMPLE
13//!   (emit native code + a `logos_yield()` at each blocking point) and straight-line code runs at native
14//!   speed with arbitrary nesting. But it needs `ucontext`/`setjmp` (host, RISC-V under qemu, a Tensix
15//!   baby core) and a full stack per task (~256 KB). This is the RISC-V tier's approach today.
16//! - [`ConcurrencyLowering::Stackless`] — a state machine: each task body is transformed into a resumable
17//!   `switch` over its blocking points, with live locals hoisted into a heap frame. UNIVERSAL (CUDA
18//!   device, WASM, embedded — no OS/libc), a tiny per-task frame, cheap function-return yields, and it
19//!   scales to many tasks. But codegen is COMPLEX (the async/await-style transform) and each resume pays
20//!   a small `switch`-dispatch cost. This is what the CUDA tier requires.
21
22/// A C-family lowering target, classified by whether it can express stackful coroutines. The
23/// concurrency knob is forced to [`ConcurrencyLowering::Stackless`] exactly when `ucontext` is absent.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum CTarget {
26    /// The host C backend (gcc/clang) — full libc, `ucontext` available.
27    HostC,
28    /// The RISC-V AOT tier (runs under qemu with glibc) — `ucontext` available.
29    RiscV,
30    /// A Tenstorrent baby RISC-V core (glibc-backed C body) — `ucontext` available.
31    Tenstorrent,
32    /// A single CUDA `<<<1,1>>>` kernel — device code has NO `ucontext`/`setjmp`.
33    Cuda,
34    /// The direct-WASM backend — no `ucontext`/`setjmp` in the sandbox.
35    Wasm,
36}
37
38impl CTarget {
39    /// Whether this target can express stackful coroutines (`getcontext`/`makecontext`/`swapcontext`).
40    /// The single capability that forces the concurrency-lowering knob.
41    pub fn has_ucontext(self) -> bool {
42        match self {
43            CTarget::HostC | CTarget::RiscV | CTarget::Tenstorrent => true,
44            CTarget::Cuda | CTarget::Wasm => false,
45        }
46    }
47}
48
49/// How the C-family backend lowers a task's suspend/resume. One point in the (one-dimensional) knob
50/// lattice; pick with [`ConcurrencyLowering::for_target`].
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ConcurrencyLowering {
53    /// Stackful coroutines via `ucontext`. Simple codegen, native speed, arbitrary nesting; needs
54    /// `ucontext` + a full stack per task. Host / RISC-V / Tenstorrent.
55    Ucontext,
56    /// A stackless state machine. Universal (device / WASM / embedded), tiny frames, cheap yields;
57    /// complex codegen + a small per-resume dispatch cost. CUDA / WASM.
58    Stackless,
59}
60
61/// One knob's trade-off — the regime where the setting **wins** and where it **loses** (why it can't be
62/// baked in unconditionally). Mirrors [`crate::codegen_cuda::KnobTradeoff`] but kept local so
63/// `codegen_c` does not depend on `codegen_cuda`.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct ConcurrencyTradeoff {
66    pub setting: &'static str,
67    pub wins: &'static str,
68    pub loses: &'static str,
69}
70
71impl ConcurrencyLowering {
72    /// The best-general lowering each target CAN express. A target without `ucontext` is FORCED to
73    /// [`ConcurrencyLowering::Stackless`]; a target that has it defaults to [`ConcurrencyLowering::Ucontext`]
74    /// (simpler codegen + native speed — the right general call when portability is not at stake).
75    pub fn for_target(target: CTarget) -> Self {
76        if target.has_ucontext() {
77            ConcurrencyLowering::Ucontext
78        } else {
79            ConcurrencyLowering::Stackless
80        }
81    }
82
83    /// The trade-off table — one row per setting, each naming where it wins and where it loses. The
84    /// concurrency-knob lock asserts this stays complete and honest (neither setting is strictly better,
85    /// which is exactly why this is a knob and not a baked-in choice).
86    pub fn tradeoffs() -> &'static [ConcurrencyTradeoff] {
87        &[
88            ConcurrencyTradeoff {
89                setting: "Ucontext",
90                wins: "targets WITH ucontext (host / RISC-V / Tenstorrent) — simple codegen (native code + \
91                       a yield at each blocking op), native straight-line speed, arbitrary loop/call nesting \
92                       across a yield",
93                loses: "no ucontext (a GPU kernel, WASM, bare metal) — cannot be expressed at all; and a full \
94                        ~256 KB stack per task, so it scales poorly to many tasks",
95            },
96            ConcurrencyTradeoff {
97                setting: "Stackless",
98                wins: "UNIVERSAL — the only option a GPU kernel / WASM / embedded target can express; a tiny \
99                       per-task frame (just the live locals), cheap function-return yields, scales to many tasks",
100                loses: "complex codegen (the async-style state-machine transform of every task body) and a \
101                        small per-resume switch-dispatch cost vs native stackful code",
102            },
103        ]
104    }
105}
106
107// ============================================================================
108//  The SCHEDULER knobs — the emitted deterministic scheduler's tunables.
109// ============================================================================
110
111pub use logicaffeine_runtime::{ClockMode, SchedulePolicy, SchedulerConfig};
112
113/// The knobs that parameterize the **emitted** deterministic scheduler. This is the SAME
114/// [`SchedulerConfig`] the oracle runtime (`logicaffeine_runtime`) runs — the ready-queue `policy`
115/// (FIFO / LIFO / round-robin / seeded-random / priority), the `clock` (virtual `Logical` vs `Wall`),
116/// the default channel capacity, and the M:N `workers` count (1 = cooperative single core, N =
117/// work-stealing) — plus the `seed` that drives the seeded chooser for the random-choice points
118/// (`TaskPick` under `Random`, `SelectWinner`, `TimerTieBreak`). Emitting C straight from a
119/// [`SchedulerConfig`] guarantees the AOT scheduler's knobs are ONE-TO-ONE with the oracle's, so a
120/// program run at `knobs` on the VM and the same `knobs` in AOT-C agree.
121///
122/// The DEFAULT — `Fifo` / `workers = 1` / `Logical` / `seed = 0` — is the exact point every
123/// determinism lock pins against `run_vm_concurrent` (which uses `SchedulerConfig::default()`), and is
124/// the fixed policy the hand-written scheduler already implements (child-first spawn, FIFO ready queue,
125/// earliest-timer-first virtual clock).
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct SchedulerKnobs {
128    /// The oracle-shared scheduler config: `policy` / `clock` / `default_channel_capacity` /
129    /// `preempt_every` / `workers`.
130    pub config: SchedulerConfig,
131    /// Seed for the emitted SplitMix64 chooser (mirrors `logicaffeine_runtime::seed::SeededRng`). Only
132    /// observable under `SchedulePolicy::Random` or on a genuine multi-ready `Select` / timer tie.
133    pub seed: u64,
134}
135
136impl Default for SchedulerKnobs {
137    fn default() -> Self {
138        SchedulerKnobs {
139            config: SchedulerConfig::default(),
140            seed: 0,
141        }
142    }
143}
144
145impl SchedulerKnobs {
146    /// The C `#define` block the emitted scheduler reads for its knobs. Prepended to the stepped tier's C.
147    /// The policy / clock enums match the emitted scheduler's `LOGOS_SCHED_*` / `LOGOS_CLOCK_*` constants
148    /// one-to-one; `logos_ready_pop` branches on `LOGOS_SCHED_POLICY`, the timer/select tie-breaks draw
149    /// from a SplitMix64 seeded by `LOGOS_SCHED_SEED`, and channels default to `LOGOS_CHAN_DEFAULT_CAP`.
150    pub fn emit_defines(&self) -> String {
151        let policy = match self.config.policy {
152            SchedulePolicy::Fifo => "LOGOS_SCHED_FIFO",
153            SchedulePolicy::Lifo => "LOGOS_SCHED_LIFO",
154            SchedulePolicy::RoundRobin => "LOGOS_SCHED_ROUNDROBIN",
155            SchedulePolicy::Random => "LOGOS_SCHED_RANDOM",
156            SchedulePolicy::Priority => "LOGOS_SCHED_PRIORITY",
157        };
158        let clock = match self.config.clock {
159            ClockMode::Logical => "LOGOS_CLOCK_LOGICAL",
160            ClockMode::Wall => "LOGOS_CLOCK_WALL",
161        };
162        format!(
163            "/* --- LOGOS scheduler knobs (mirror logicaffeine_runtime::SchedulerConfig) --------------- */\n\
164             #define LOGOS_SCHED_FIFO 0\n\
165             #define LOGOS_SCHED_LIFO 1\n\
166             #define LOGOS_SCHED_ROUNDROBIN 2\n\
167             #define LOGOS_SCHED_RANDOM 3\n\
168             #define LOGOS_SCHED_PRIORITY 4\n\
169             #define LOGOS_CLOCK_LOGICAL 0\n\
170             #define LOGOS_CLOCK_WALL 1\n\
171             #define LOGOS_SCHED_POLICY {policy}\n\
172             #define LOGOS_SCHED_CLOCK {clock}\n\
173             #define LOGOS_SCHED_WORKERS {workers}\n\
174             #define LOGOS_SCHED_SEED {seed}ULL\n\
175             #define LOGOS_CHAN_DEFAULT_CAP {cap}LL\n",
176            workers = self.config.workers.max(1),
177            seed = self.seed,
178            cap = self.config.default_channel_capacity,
179        )
180    }
181}
182
183#[cfg(test)]
184#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_c/concurrency/tests.rs"]
185mod tests;