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}