diff --git a/design/sans-io-comparison.md b/design/sans-io-comparison.md index b59242b..bb5f6b4 100644 --- a/design/sans-io-comparison.md +++ b/design/sans-io-comparison.md @@ -106,7 +106,7 @@ impl Counter { ### `PollOnce` (compiler-generated state machine) ```rust -fn get_timestamped(&self) -> BoxFuture<'_, (u64, u64)> { +fn value_at_now(&self) -> BoxFuture<'_, (u64, u64)> { let val = self.current; let effects = &self.effects; @@ -134,18 +134,11 @@ The compiler turns the `async move { ... }` block into an enum with states at ea | _Effect protocol_ | Explicit `Effect`/`Event` enums | Shared-state slots + `poll_fn` | | _Composability_ | Must manually compose states | `async`/`await` composes naturally | | _Rust runtime reuse_ | Separate impl for async Rust | Same impl works with `.await` or `poll_once()` | -| _Inspection_ | All states visible in code | States are opaque (compiler-generated) | +| _Inspection_ | All states visible in code | States are compiler-generated | | _Testability_ | Unit-test each state transition | Same shape; also testable via `.await` ([details](#testing)) | | _Cancellation_ | Drop the enum | Drop the `BoxFuture` | | _Cost_ | No heap allocation | One `Box::pin` per future | -### When to prefer pure sans-IO - -- **Debuggability is paramount.** Hand-coded states are nameable and inspectable; you can log "entered state NeedTimestamp" or serialize the machine. -- **No heap allocation allowed.** Embedded or `#[no_alloc]` contexts where `Box::pin` is off the table. -- **Protocol has complex branching.** When the state graph doesn't map cleanly to sequential `async` code (e.g., many concurrent sub-protocols interleaved), explicit state enums can be clearer. -- **Deterministic replay.** Pure sans-IO machines are easy to serialize, snapshot, and replay because all inputs and outputs flow through the `step` function. - ### When to prefer `PollOnce` - **Code reuse across runtimes.** The same trait impl works for tokio (`.await`) _and_ FFI (`poll_once()`). No separate implementation for each execution model. @@ -153,6 +146,13 @@ The compiler turns the `async move { ... }` block into an enum with states at ea - **Rapid iteration.** Adding a new effect to an `async` function is one `poll_fn(...).await` call. Adding it to a hand-coded state machine means a new state variant, new match arms, and updating the transition table. - **Existing async ecosystem.** If your logic already uses `async`/`await` (e.g., you're wrapping an existing async library), `PollOnce` lets FFI hosts drive it without rewriting. +### When to prefer pure sans-IO + +- **Debuggability is paramount.** Hand-coded states are nameable and inspectable; you can log "entered state NeedTimestamp" or serialize the machine. +- **No heap allocation allowed.** Embedded or `#[no_alloc]` contexts where `Box::pin` is off the table. +- **Protocol has complex branching.** When the state graph doesn't map cleanly to sequential `async` code (e.g., many concurrent sub-protocols interleaved), explicit state enums can be clearer. +- **Deterministic replay.** Pure sans-IO machines are easy to serialize, snapshot, and replay because all inputs and outputs flow through the `step` function. + ## Testing Testing looks similar in both approaches: you step the machine, assert on effects, provide responses, step again, and assert on the result. The test structure mirrors the host loop because _the test is acting as the host_. @@ -163,7 +163,7 @@ With a hand-coded state machine, tests call `step()` directly and pattern-match ```rust #[test] -fn get_timestamped_returns_value_and_timestamp() { +fn value_at_now_returns_value_and_timestamp() { let mut counter = Counter::new(42); // Step 1: machine requests a timestamp. @@ -202,10 +202,10 @@ use future_form::{FutureForm, Sendable}; use future_form::host::PollOnce; #[test] -fn get_timestamped_returns_value_and_timestamp() { +fn value_at_now_returns_value_and_timestamp() { let counter = EffectCounter::new(42); let mut fut = - >::get_timestamped(&counter); + >::value_at_now(&counter); // Step 1: future is pending (needs timestamp from host). assert_eq!(fut.poll_once(), Poll::Pending); @@ -258,7 +258,7 @@ One advantage of `PollOnce`: the same implementation can also be tested with an ```rust #[tokio::test] -async fn get_timestamped_with_runtime() { +async fn value_at_now_with_runtime() { let counter = EffectCounter::new(42); // Spawn a task that fulfills the effect. @@ -275,7 +275,7 @@ async fn get_timestamped_with_runtime() { }); let (val, ts) = - >::get_timestamped(&counter) + >::value_at_now(&counter) .await; assert_eq!((val, ts), (42, 1234567890)); diff --git a/examples/ffi/counter-effects/src/lib.rs b/examples/ffi/counter-effects/src/lib.rs index a89c40b..b88fc46 100644 --- a/examples/ffi/counter-effects/src/lib.rs +++ b/examples/ffi/counter-effects/src/lib.rs @@ -106,7 +106,7 @@ impl Default for EffectSlot { /// A counter that requests timestamps and logging from the host. pub trait TimestampedCounter { /// Return `(current_value, host_timestamp)`. - fn get_timestamped(&self) -> K::Future<'_, (u64, u64)>; + fn value_at_now(&self) -> K::Future<'_, (u64, u64)>; /// Add `n`, ask the host to log the operation, return new value. fn add_with_log(&self, n: u64) -> K::Future<'_, u64>; @@ -132,7 +132,7 @@ impl EffectCounter { #[future_form(Sendable)] impl TimestampedCounter for EffectCounter { - fn get_timestamped(&self) -> K::Future<'_, (u64, u64)> { + fn value_at_now(&self) -> K::Future<'_, (u64, u64)> { let val = self.current; let effects = &self.effects; let effect = Effect::GetTimestamp; @@ -176,10 +176,10 @@ mod tests { use super::*; #[test] - fn get_timestamped_effect_protocol() { + fn value_at_now_effect_protocol() { let counter = EffectCounter::new(42); let mut fut = - >::get_timestamped(&counter); + >::value_at_now(&counter); // First poll: should be pending (needs timestamp from host). assert_eq!(fut.poll_once(), Poll::Pending); diff --git a/examples/ffi/effects-bridge/src/lib.rs b/examples/ffi/effects-bridge/src/lib.rs index 1350884..65048f6 100644 --- a/examples/ffi/effects-bridge/src/lib.rs +++ b/examples/ffi/effects-bridge/src/lib.rs @@ -8,7 +8,7 @@ //! ```text //! Host Rust //! │ │ -//! │ counter_get_timestamped │ +//! │ counter_value_at_now │ //! │ ─────────────────────────>│ creates BoxFuture //! │ │ //! │ effects_future_poll │ @@ -59,7 +59,7 @@ const EFFECT_LOG: u8 = 2; /// to find out what the future needs. /// /// When `status != 0` (ready), `value_a` and `value_b` hold the result. -/// For `get_timestamped`: `value_a` = counter value, `value_b` = timestamp. +/// For `value_at_now`: `value_a` = counter value, `value_b` = timestamp. /// For `add_with_log`: `value_a` = result, `value_b` = 0. #[repr(C)] pub struct EffectPollResult { @@ -122,12 +122,12 @@ pub unsafe extern "C" fn effects_counter_free(ptr: *mut EffectCounter) { /// # Safety /// `counter` must be a valid pointer returned by `effects_counter_new`. #[unsafe(no_mangle)] -pub unsafe extern "C" fn counter_get_timestamped( +pub unsafe extern "C" fn counter_value_at_now( counter: *const EffectCounter, ) -> *mut EffectFutureHandle { let c = unsafe { &*counter }; let fut: BoxFuture<'_, (u64, u64)> = - >::get_timestamped(c); + >::value_at_now(c); // SAFETY: EffectCounter copies values into the async block — the future // holds references to the EffectSlot (which lives inside the counter), @@ -173,7 +173,7 @@ pub unsafe extern "C" fn counter_add_with_log( /// future needs, fulfill it, then re-poll. /// /// # Safety -/// `handle` must be a valid pointer from `counter_get_timestamped` or +/// `handle` must be a valid pointer from `counter_value_at_now` or /// `counter_add_with_log`. #[unsafe(no_mangle)] pub unsafe extern "C" fn effects_future_poll( @@ -286,7 +286,7 @@ pub unsafe extern "C" fn fulfill_log(handle: *const EffectFutureHandle) { // --------------------------------------------------------------------------- /// # Safety -/// `handle` must be a valid pointer from `counter_get_timestamped` or +/// `handle` must be a valid pointer from `counter_value_at_now` or /// `counter_add_with_log`, and not yet freed. #[unsafe(no_mangle)] pub unsafe extern "C" fn effects_future_free(handle: *mut EffectFutureHandle) { @@ -318,12 +318,12 @@ pub unsafe extern "system" fn Java_EffectsHost_effectsCounterFree( } #[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_EffectsHost_counterGetTimestamped( +pub unsafe extern "system" fn Java_EffectsHost_counterValueAtNow( _env: JNIEnv<'_>, _class: JClass<'_>, counter_handle: jlong, ) -> jlong { - unsafe { counter_get_timestamped(counter_handle as *const EffectCounter) as jlong } + unsafe { counter_value_at_now(counter_handle as *const EffectCounter) as jlong } } #[unsafe(no_mangle)] diff --git a/examples/ffi/go-effects-host/main.go b/examples/ffi/go-effects-host/main.go index 19f9d2f..7d5f29c 100644 --- a/examples/ffi/go-effects-host/main.go +++ b/examples/ffi/go-effects-host/main.go @@ -37,7 +37,7 @@ EffectsCounterHandle effects_counter_new(uint64_t start); void effects_counter_free(EffectsCounterHandle counter); // Start async operations. -EffectsFutureHandle counter_get_timestamped(EffectsCounterHandle counter); +EffectsFutureHandle counter_value_at_now(EffectsCounterHandle counter); EffectsFutureHandle counter_add_with_log(EffectsCounterHandle counter, uint64_t n); // Polling + effect protocol. @@ -114,16 +114,16 @@ func main() { counter := C.effects_counter_new(10) - // --- Test 1: get_timestamped (value=10, timestamp>0) --- - fut := C.counter_get_timestamped(counter) + // --- Test 1: value_at_now (value=10, timestamp>0) --- + fut := C.counter_value_at_now(counter) val, ts := pollWithEffects(fut) C.effects_future_free(fut) - check("get_timestamped value", val, 10) + check("value_at_now value", val, 10) if ts > 0 { - fmt.Printf(" PASS get_timestamped timestamp: %d ms\n", ts) + fmt.Printf(" PASS value_at_now timestamp: %d ms\n", ts) passed++ } else { - fmt.Printf(" FAIL get_timestamped timestamp: got %d, want > 0\n", ts) + fmt.Printf(" FAIL value_at_now timestamp: got %d, want > 0\n", ts) failed++ } @@ -137,15 +137,15 @@ func main() { C.effects_counter_free(counter) counter = C.effects_counter_new(100) - fut = C.counter_get_timestamped(counter) + fut = C.counter_value_at_now(counter) val, ts = pollWithEffects(fut) C.effects_future_free(fut) - check("get_timestamped(100) value", val, 100) + check("value_at_now(100) value", val, 100) if ts > 0 { - fmt.Printf(" PASS get_timestamped(100) timestamp: %d ms\n", ts) + fmt.Printf(" PASS value_at_now(100) timestamp: %d ms\n", ts) passed++ } else { - fmt.Printf(" FAIL get_timestamped(100) timestamp: got %d, want > 0\n", ts) + fmt.Printf(" FAIL value_at_now(100) timestamp: got %d, want > 0\n", ts) failed++ } diff --git a/examples/ffi/java-effects-host/EffectsHost.java b/examples/ffi/java-effects-host/EffectsHost.java index 5d5c064..acd0165 100644 --- a/examples/ffi/java-effects-host/EffectsHost.java +++ b/examples/ffi/java-effects-host/EffectsHost.java @@ -18,7 +18,7 @@ public class EffectsHost { private static native long effectsCounterNew(long start); private static native void effectsCounterFree(long handle); - private static native long counterGetTimestamped(long counterHandle); + private static native long counterValueAtNow(long counterHandle); private static native long counterAddWithLog(long counterHandle, long n); /** @@ -87,16 +87,16 @@ public class EffectsHost { long counter = effectsCounterNew(10); - // --- Test 1: get_timestamped (value=10, timestamp>0) --- - long fut = counterGetTimestamped(counter); + // --- Test 1: value_at_now (value=10, timestamp>0) --- + long fut = counterValueAtNow(counter); long[] result = pollWithEffects(fut); effectsFutureFree(fut); - check("get_timestamped value", result[0], 10); + check("value_at_now value", result[0], 10); if (result[1] > 0) { - System.out.printf(" PASS get_timestamped timestamp: %d ms%n", result[1]); + System.out.printf(" PASS value_at_now timestamp: %d ms%n", result[1]); passed++; } else { - System.out.printf(" FAIL get_timestamped timestamp: got %d, want > 0%n", result[1]); + System.out.printf(" FAIL value_at_now timestamp: got %d, want > 0%n", result[1]); failed++; } @@ -110,15 +110,15 @@ public class EffectsHost { effectsCounterFree(counter); counter = effectsCounterNew(100); - fut = counterGetTimestamped(counter); + fut = counterValueAtNow(counter); result = pollWithEffects(fut); effectsFutureFree(fut); - check("get_timestamped(100) value", result[0], 100); + check("value_at_now(100) value", result[0], 100); if (result[1] > 0) { - System.out.printf(" PASS get_timestamped(100) timestamp: %d ms%n", result[1]); + System.out.printf(" PASS value_at_now(100) timestamp: %d ms%n", result[1]); passed++; } else { - System.out.printf(" FAIL get_timestamped(100) timestamp: got %d, want > 0%n", result[1]); + System.out.printf(" FAIL value_at_now(100) timestamp: got %d, want > 0%n", result[1]); failed++; } diff --git a/examples/ffi/python-effects-host/effects_host.py b/examples/ffi/python-effects-host/effects_host.py index bc988d9..ad1308c 100644 --- a/examples/ffi/python-effects-host/effects_host.py +++ b/examples/ffi/python-effects-host/effects_host.py @@ -56,8 +56,8 @@ lib.effects_counter_new.restype = c_void_p lib.effects_counter_free.argtypes = [c_void_p] lib.effects_counter_free.restype = None -lib.counter_get_timestamped.argtypes = [c_void_p] -lib.counter_get_timestamped.restype = c_void_p +lib.counter_value_at_now.argtypes = [c_void_p] +lib.counter_value_at_now.restype = c_void_p lib.counter_add_with_log.argtypes = [c_void_p, c_uint64] lib.counter_add_with_log.restype = c_void_p @@ -137,16 +137,16 @@ def main(): counter = lib.effects_counter_new(10) - # --- Test 1: get_timestamped (value=10, timestamp>0) --- - fut = lib.counter_get_timestamped(counter) + # --- Test 1: value_at_now (value=10, timestamp>0) --- + fut = lib.counter_value_at_now(counter) val, ts = poll_with_effects(fut) lib.effects_future_free(fut) - check("get_timestamped value", val, 10) + check("value_at_now value", val, 10) if ts > 0: - print(f" PASS get_timestamped timestamp: {ts} ms") + print(f" PASS value_at_now timestamp: {ts} ms") passed += 1 else: - print(f" FAIL get_timestamped timestamp: got {ts}, want > 0") + print(f" FAIL value_at_now timestamp: got {ts}, want > 0") failed += 1 # --- Test 2: add_with_log (10 + 32 = 42, should print log) --- @@ -159,15 +159,15 @@ def main(): lib.effects_counter_free(counter) counter = lib.effects_counter_new(100) - fut = lib.counter_get_timestamped(counter) + fut = lib.counter_value_at_now(counter) val, ts = poll_with_effects(fut) lib.effects_future_free(fut) - check("get_timestamped(100) value", val, 100) + check("value_at_now(100) value", val, 100) if ts > 0: - print(f" PASS get_timestamped(100) timestamp: {ts} ms") + print(f" PASS value_at_now(100) timestamp: {ts} ms") passed += 1 else: - print(f" FAIL get_timestamped(100) timestamp: got {ts}, want > 0") + print(f" FAIL value_at_now(100) timestamp: got {ts}, want > 0") failed += 1 fut = lib.counter_add_with_log(counter, 900)