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;