Skip to main content

kithara_platform/common/
flash_inert.rs

1pub use std::time::Duration;
2use std::{
3    marker::PhantomData,
4    panic::Location,
5    pin::Pin,
6    task::{Context, Poll},
7};
8
9pub use crate::common::time::Instant;
10use crate::common::time::TimeoutError;
11// Functional references (`sleep`/`timeout` below, `crate::thread::park_timeout`
12// in `virtual_park_timeout`) deliberately stay on the lane-gated root glob:
13// the inert `virtual_*` aliases ARE whichever backend owns the lane (native or
14// wasm), so they must resolve through it. Typed imports above come from
15// `common::time` directly (the backends re-export the same types). The glob
16// dependency is a known wart; it dies with the W3/W5 layering work.
17use crate::time::{sleep, timeout};
18
19/// Off the sim path a spawned task needs no quiescence accounting, so the
20/// participant wrapper is the future itself. Under `flash` this is the
21/// engine's gate wrapper counted in `active_async`.
22pub type Participating<F> = F;
23
24/// Off the sim path: spawning needs no quiescence bracket, so `participate` is
25/// an identity passthrough (the real clock already advances on its own). Under
26/// `flash` this is the engine's `participate`, which wraps the future so it
27/// counts in the engine's `active_async` while running. The `loc` spawn-site
28/// identity (used by the engine's hang dump) is unused off the sim path.
29#[inline]
30pub fn participate<F: Future>(fut: F, _loc: &'static Location<'static>) -> Participating<F> {
31    fut
32}
33
34/// Off the sim path ambient does not exist, so there is nothing to re-assert
35/// per poll: the wrapper is the future itself. Under `flash` this is the
36/// engine's per-poll ambient re-assert wrapper.
37pub type WithAmbient<F> = F;
38
39/// Off the sim path: ambient does not exist, so re-asserting it per poll is an
40/// identity passthrough. Under `flash` this is the engine's `with_ambient`,
41/// which re-establishes the snapshotted ambient around every poll so a future
42/// (e.g. the async test body) keeps its flash-eligibility across `.await`
43/// thread-hops instead of relying on a one-shot guard set on the first poll.
44#[inline]
45pub fn with_ambient<F: Future>(_on: bool, fut: F) -> WithAmbient<F> {
46    fut
47}
48
49/// No-op real-time scope off the sim path (time is already real). `!Send`
50/// (`PhantomData<*mut ()>`) for auto-trait PARITY with the engine guard: a
51/// consumer compiling against the inert form must not become Send-legal code
52/// that fails to compile under `flash`.
53#[derive(Debug)]
54pub struct FlashScope {
55    _not_send: PhantomData<*mut ()>,
56}
57
58/// Enter a real-time scope. Off the sim path this is a ZST no-op; under
59/// `flash` it puts the current thread on real time for the guard's lifetime.
60#[inline]
61#[must_use]
62pub fn flash_real() -> FlashScope {
63    FlashScope {
64        _not_send: PhantomData,
65    }
66}
67
68/// Off the sim path: a prod `#[kithara::flash(bool)]` sync region's RAII guard is
69/// a ZST no-op (time is already real), so an annotated fn compiles away to its
70/// bare body. Under `flash` this is the engine's `enter_dynamic`.
71#[inline]
72#[must_use]
73pub fn enter_dynamic(_on: bool) -> FlashScope {
74    FlashScope {
75        _not_send: PhantomData,
76    }
77}
78
79/// Off the sim path a prod async `#[kithara::flash(bool)]` region needs no
80/// per-poll mode re-assert: the wrapper is the future itself. Under `flash`
81/// this is the engine's per-poll dynamic-flash wrapper.
82pub type FlashDynamic<F> = F;
83
84/// Off the sim path: a prod `#[kithara::flash(bool)]` async region is an identity
85/// passthrough (no per-poll re-assert needed when time is already real). Under
86/// `flash` this is the engine's `dynamic`.
87#[inline]
88pub fn dynamic<F: Future>(_on: bool, fut: F) -> FlashDynamic<F> {
89    fut
90}
91
92/// Inert mirror of the engine-backed yield future. Never produced by the inert
93/// [`yield_now`] (off the sim path the real arm is always taken); present so
94/// the surface matches the flash control surface 1:1.
95pub struct FlashYield {
96    _priv: (),
97}
98
99impl Future for FlashYield {
100    type Output = ();
101
102    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
103        Poll::Ready(())
104    }
105}
106
107/// Cooperative async yield. Off the sim path this always takes the real arm —
108/// the same semantics the engine build uses outside a flash-eligible test:
109/// hand control back to the scheduler once, then resolve.
110pub fn yield_now() -> Yield {
111    Yield::Real { yielded: false }
112}
113
114/// Cooperative yield future (see [`yield_now`]). Mirrors the flash enum; the
115/// [`Yield::Flash`] variant is never constructed off the sim path.
116#[must_use = "a Yield future does nothing unless `.await`ed"]
117pub enum Yield {
118    /// Engine-backed quiescence yield: surface parity only, never built here.
119    Flash(FlashYield),
120    /// Real cooperative yield: returns `Pending` once after re-arming the
121    /// waker, then `Ready` — the same hand-back-to-the-scheduler semantics as
122    /// `tokio::task::yield_now`.
123    Real { yielded: bool },
124}
125
126impl Future for Yield {
127    type Output = ();
128
129    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
130        match self.get_mut() {
131            Self::Flash(f) => Pin::new(f).poll(cx),
132            Self::Real { yielded } => {
133                if *yielded {
134                    Poll::Ready(())
135                } else {
136                    *yielded = true;
137                    cx.waker().wake_by_ref();
138                    Poll::Pending
139                }
140            }
141        }
142    }
143}
144
145/// Off the sim path the lexical test rewriter's `virtual_*` targets alias
146/// the REAL primitives, so a rewritten test body behaves identically to its
147/// unrewritten form (the rewrite is a no-op when `flash` is off). The
148/// `#[kithara::test]` macro emits these into EVERY test body, so they must
149/// resolve in the off-feature + wasm configs.
150#[inline]
151pub fn virtual_sleep(duration: Duration) -> impl Future<Output = ()> {
152    sleep(duration)
153}
154
155/// Off-feature real alias for the rewriter's virtual `timeout` (see
156/// [`virtual_sleep`]).
157///
158/// # Errors
159///
160/// Returns [`TimeoutError`] if the future does not complete within `duration`.
161#[inline]
162pub async fn virtual_timeout<F>(duration: Duration, future: F) -> Result<F::Output, TimeoutError>
163where
164    F: Future,
165{
166    timeout(duration, future).await
167}
168
169/// Off-feature real alias for the rewriter's virtual `Instant::now` (see
170/// [`virtual_sleep`]).
171#[inline]
172#[must_use]
173pub fn virtual_now() -> Instant {
174    Instant::now()
175}
176
177/// Off-feature real alias for the rewriter's virtual `park_timeout` (see
178/// [`virtual_sleep`]).
179#[inline]
180pub fn virtual_park_timeout(duration: Duration) {
181    crate::thread::park_timeout(duration);
182}
183
184/// No virtual timeline or quiescence engine to reset off the sim path (the
185/// clock is real), so this is a no-op.
186#[inline]
187pub fn reset() {}
188
189/// Off the sim path a real I/O operation needs no pacing (time is already
190/// real), so the scope is a ZST no-op. Under `flash` it is the engine's
191/// `RealIoScope`: while held, the virtual clock may not outrun real
192/// time, so virtual watchdogs/timeouts cannot fire spuriously ahead of bytes
193/// still on the wire.
194#[derive(Debug)]
195pub struct RealIoScope;
196
197/// Bracket ONE real I/O operation. Off the sim path this is a ZST no-op;
198/// under `flash` it paces the virtual clock to real time while held.
199#[inline]
200#[must_use]
201pub fn real_io() -> RealIoScope {
202    RealIoScope
203}
204
205/// No-op per-test ambient gate off the sim path (time is already real). The
206/// `#[kithara::test]` macro emits `ambient_scope(..)` into sync and wasm test
207/// bodies (async-native bodies carry `with_ambient` per-poll instead), so the
208/// guard must exist (as a ZST) in the off-feature + wasm configs. Under
209/// `flash` this is the engine's `AmbientScope` / `ambient_scope`. `!Send` for
210/// auto-trait parity with the engine guard (see [`FlashScope`]).
211#[derive(Debug)]
212pub struct AmbientScope {
213    _not_send: PhantomData<*mut ()>,
214}
215
216/// Set the per-test ambient gate. Off the sim path this is a ZST no-op (time is
217/// already real); under `flash` it is the engine's `ambient_scope`.
218#[inline]
219#[must_use]
220pub fn ambient_scope(_on: bool) -> AmbientScope {
221    AmbientScope {
222        _not_send: PhantomData,
223    }
224}
225
226/// Snapshot the per-test ambient gate for spawn propagation. Off the sim path
227/// the gate does not exist and no test is flash-eligible, so the snapshot is
228/// always `false` (the engine's default outside a flash test).
229#[inline]
230#[must_use]
231pub fn ambient_snapshot() -> bool {
232    false
233}
234
235/// Restore a snapshotted ambient on a spawned child. Off the sim path this is
236/// the ZST no-op guard (see [`ambient_scope`]); under `flash` it re-establishes
237/// the engine's per-test gate for the child's lifetime.
238#[inline]
239#[must_use]
240pub fn set_ambient_for_spawn(_on: bool) -> AmbientScope {
241    AmbientScope {
242        _not_send: PhantomData,
243    }
244}
245
246/// No engine to report without the `flash` feature (or on wasm).
247#[must_use]
248pub fn hang_dump(_context: &str) -> String {
249    String::new()
250}
251
252/// No engine to report without the `flash` feature (or on wasm).
253pub fn log_hang_dump(_context: &str) {}