grafos_std/grafos_worker_v0.rs
1//! `grafos_worker_v0` — guest-facing WASM ABI for shared-memory tasklets.
2//!
3//! This module declares the import contract that a shared-memory tasklet
4//! guest binary uses to talk to the worker runtime: worker identity, the
5//! single tasklet-wide barrier, cancellation observation, and the linear
6//! memory layout of the shared region and per-worker scratch.
7//!
8//! The Phase 48.3 P3 deliverable is the *SDK surface and ABI declaration
9//! only* — the runtime that backs these symbols is implemented separately
10//! (P1). On `wasm32` targets the externs link to host-provided imports.
11//! On native targets a small mock backs them so unit tests can exercise
12//! the SDK without a live runtime.
13//!
14//! See `docs/grafos-tasklet-abi-v1.md` for the normative spec.
15
16#![allow(clippy::missing_safety_doc)]
17
18// ---------------------------------------------------------------------------
19// WASM ABI — real imports (only present when compiled to wasm32).
20// ---------------------------------------------------------------------------
21
22#[cfg(target_arch = "wasm32")]
23#[link(wasm_import_module = "grafos_worker_v0")]
24extern "C" {
25 /// Returns this worker's lane index in `[0, fb_worker_count())`.
26 pub fn fb_worker_index() -> i32;
27
28 /// Returns the total number of workers participating in this tasklet.
29 pub fn fb_worker_count() -> i32;
30
31 /// Block until all workers have reached the implicit tasklet-wide barrier.
32 ///
33 /// Returns `0` on normal release, `-1` if the tasklet was cancelled while
34 /// waiting (workers must observe and exit cooperatively).
35 pub fn fb_barrier_wait() -> i32;
36
37 /// Returns `1` if the tasklet has been cancelled (revoke / expiry /
38 /// explicit cancel), `0` otherwise.
39 pub fn fb_tasklet_cancelled() -> i32;
40
41 /// Offset into linear memory of the shared region.
42 pub fn fb_shared_ptr() -> i32;
43 /// Length in bytes of the shared region.
44 pub fn fb_shared_len() -> i32;
45
46 /// Offset into linear memory of *this* worker's scratch region.
47 pub fn fb_scratch_ptr() -> i32;
48 /// Length in bytes of *this* worker's scratch region.
49 pub fn fb_scratch_len() -> i32;
50
51 /// Cooperatively reconcile fuel with the lease-wide shared fuel pool.
52 ///
53 /// Decrements the shared pool by `amount` and refills this Store's
54 /// local fuel by `amount`. Returns 0 on success, -1 on pool exhaustion
55 /// or programmer error (negative `amount`). Calling with `amount == 0`
56 /// is a silent no-op returning 0.
57 ///
58 /// V2 (shared-pool) workers MUST call this at safe points within any
59 /// compute loop that may consume more fuel than the initial precharge.
60 /// On V1 (per-worker-slice) workers this hostcall is bound but always
61 /// returns -1 — V1 modules should not import or call it.
62 ///
63 /// Phase 48.13: declared in W1; host runtime implementation lands in
64 /// W2a (fabricbiosd shared-memory dispatcher, wasmtime Caller::set_fuel).
65 pub fn fb_fuel_checkpoint(amount: i64) -> i32;
66}
67
68// ---------------------------------------------------------------------------
69// Native shim — host-side mock used by SDK unit tests.
70//
71// This is NOT the runtime. It exists solely so `grafos-std` unit tests can
72// instantiate `CoordinatorCtx` / `WorkerCtx` and verify that the SDK plumbing
73// behaves correctly. P1 will provide the real wasmtime/wasmi-backed runtime.
74// ---------------------------------------------------------------------------
75
76#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
77pub use mock::*;
78
79#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
80pub mod mock {
81 //! Host-side mock for `grafos_worker_v0`.
82 //!
83 //! Backed by a thread-local `WorkerSlot`. Tests (or the SDK's
84 //! `SharedTaskletBuilder` mock launch path) install a slot via
85 //! [`with_worker_slot`] before calling any `fb_*` shim.
86
87 extern crate alloc;
88 use alloc::sync::Arc;
89 use core::cell::RefCell;
90 use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
91 use std::sync::Barrier;
92
93 /// State visible to a single worker lane (or the coordinator) inside the
94 /// mock runtime.
95 #[derive(Clone)]
96 pub struct WorkerSlot {
97 pub worker_index: i32,
98 pub worker_count: i32,
99 pub cancelled: Arc<AtomicBool>,
100 /// Optional cross-thread barrier. `None` for single-threaded mock
101 /// launches (where `barrier()` is a no-op).
102 pub barrier: Option<Arc<Barrier>>,
103 /// Offsets into a notional linear memory. The mock is intentionally
104 /// faithful to the wasm contract: shared region first, then scratch
105 /// regions in worker-index order.
106 pub shared_ptr: i32,
107 pub shared_len: i32,
108 pub scratch_ptr: i32,
109 pub scratch_len: i32,
110 /// Phase 48.13 W2b — lease-wide shared fuel pool for V2
111 /// shared-memory tasklets. `None` simulates a V1 worker (or any
112 /// caller that hasn't installed a pool); the mock
113 /// `fb_fuel_checkpoint` returns -1 in that case so V1 source
114 /// observing the spec'd behavior fails closed.
115 pub shared_fuel_pool: Option<Arc<AtomicU64>>,
116 }
117
118 impl Default for WorkerSlot {
119 fn default() -> Self {
120 Self {
121 worker_index: 0,
122 worker_count: 1,
123 cancelled: Arc::new(AtomicBool::new(false)),
124 barrier: None,
125 shared_ptr: 0,
126 shared_len: 0,
127 scratch_ptr: 0,
128 scratch_len: 0,
129 shared_fuel_pool: None,
130 }
131 }
132 }
133
134 thread_local! {
135 static SLOT: RefCell<Option<WorkerSlot>> = const { RefCell::new(None) };
136 }
137
138 /// Install `slot` for the duration of `f` (this thread only).
139 pub fn with_worker_slot<R>(slot: WorkerSlot, f: impl FnOnce() -> R) -> R {
140 let prev = SLOT.with(|s| s.borrow_mut().replace(slot));
141 let out = f();
142 SLOT.with(|s| *s.borrow_mut() = prev);
143 out
144 }
145
146 fn slot<R>(f: impl FnOnce(&WorkerSlot) -> R) -> R {
147 SLOT.with(|s| {
148 let b = s.borrow();
149 let slot = b
150 .as_ref()
151 .expect("grafos_worker_v0 mock: no WorkerSlot installed on this thread");
152 f(slot)
153 })
154 }
155
156 pub fn fb_worker_index() -> i32 {
157 slot(|s| s.worker_index)
158 }
159 pub fn fb_worker_count() -> i32 {
160 slot(|s| s.worker_count)
161 }
162 pub fn fb_tasklet_cancelled() -> i32 {
163 slot(|s| {
164 if s.cancelled.load(Ordering::SeqCst) {
165 1
166 } else {
167 0
168 }
169 })
170 }
171 pub fn fb_barrier_wait() -> i32 {
172 let (barrier, cancelled) = slot(|s| (s.barrier.clone(), s.cancelled.clone()));
173 if cancelled.load(Ordering::SeqCst) {
174 return -1;
175 }
176 if let Some(b) = barrier {
177 b.wait();
178 }
179 if cancelled.load(Ordering::SeqCst) {
180 -1
181 } else {
182 0
183 }
184 }
185 pub fn fb_shared_ptr() -> i32 {
186 slot(|s| s.shared_ptr)
187 }
188 pub fn fb_shared_len() -> i32 {
189 slot(|s| s.shared_len)
190 }
191 pub fn fb_scratch_ptr() -> i32 {
192 slot(|s| s.scratch_ptr)
193 }
194 pub fn fb_scratch_len() -> i32 {
195 slot(|s| s.scratch_len)
196 }
197
198 /// Phase 48.13 W2b mock implementation of `fb_fuel_checkpoint`.
199 ///
200 /// Consults the per-thread [`WorkerSlot`]'s `shared_fuel_pool` and
201 /// performs a CAS-loop decrement that mirrors the wasmtime runtime
202 /// behavior in `fabricbiosd`'s shared-memory dispatcher. The mock does
203 /// not actually consume host-side native fuel — it only tracks the
204 /// logical pool state — but the success/failure semantics match the
205 /// real runtime so SDK and program tests can exercise pool exhaustion
206 /// deterministically.
207 ///
208 /// Returns:
209 /// * `amount == 0` → `0` (silent no-op, matches spec)
210 /// * `amount < 0` → `-1` (programmer error, matches spec)
211 /// * No pool installed (V1 worker simulation, or no slot) → `-1`
212 /// * Pool exhausted (`pool < amount`) → `-1`
213 /// * Successful debit → `0`
214 pub fn fb_fuel_checkpoint(amount: i64) -> i32 {
215 if amount == 0 {
216 return 0;
217 }
218 if amount < 0 {
219 return -1;
220 }
221 let amount = amount as u64;
222 let pool = SLOT.with(|s| {
223 s.borrow()
224 .as_ref()
225 .and_then(|slot| slot.shared_fuel_pool.clone())
226 });
227 let pool = match pool {
228 Some(p) => p,
229 None => return -1,
230 };
231 let mut current = pool.load(Ordering::SeqCst);
232 loop {
233 if current < amount {
234 return -1;
235 }
236 match pool.compare_exchange_weak(
237 current,
238 current - amount,
239 Ordering::SeqCst,
240 Ordering::SeqCst,
241 ) {
242 Ok(_) => return 0,
243 Err(observed) => current = observed,
244 }
245 }
246 }
247
248 /// Test helper: install a single-thread slot, run `f`, restore.
249 pub fn mock_set_single_worker(worker_count: u16) {
250 let slot = WorkerSlot {
251 worker_index: 0,
252 worker_count: worker_count as i32,
253 ..WorkerSlot::default()
254 };
255 SLOT.with(|s| *s.borrow_mut() = Some(slot));
256 }
257
258 /// Test helper: clear any installed slot.
259 pub fn mock_clear() {
260 SLOT.with(|s| *s.borrow_mut() = None);
261 }
262}