Skip to main content

oxilean_kernel/
wall_clock.rs

1//! A tiny, portable monotonic-clock shim used across the TCB crates.
2//!
3//! # Why this exists
4//!
5//! The kernel, the export reader, and the verify engine all use this module's
6//! [`Instant`](crate::wall_clock::Instant) for two purposes:
7//!
8//! * cosmetic per-declaration timing (the `micros` a verdict carries), and
9//! * termination watchdogs / fuel refill (elapsed-time bounds on reduction).
10//!
11//! On the `wasm32-unknown-unknown` target — the one the *Kernel in a Tab* demo
12//! compiles for — the standard library's `Instant::now()` **panics** with
13//! *"time not implemented on this platform"* (`std::sys::time::unsupported`).
14//! Because the very first thing the verify engine does is take a timestamp, that
15//! panic makes the entire checking path dead code: the optimizer folds
16//! everything after the panic into `unreachable`, dead-code elimination strips
17//! the kernel, and the resulting `.wasm` is a tiny stub that verifies nothing.
18//! That is precisely the silent-empty-artifact failure the engineering brief
19//! (§8.1) warns about.
20//!
21//! # What this provides
22//!
23//! [`Instant`](crate::wall_clock::Instant) — a drop-in for the subset of the standard `Instant` these crates
24//! use: [`Instant::now`](crate::wall_clock::Instant::now), [`Instant::elapsed`](crate::wall_clock::Instant::elapsed), and [`Instant::duration_since`](crate::wall_clock::Instant::duration_since),
25//! all returning [`core::time::Duration`].
26//!
27//! * **Off wasm** (every native build, the entire test suite, the CLI, docs.rs)
28//!   it is a *transparent* newtype over the standard library `Instant`, so native
29//!   timing and every watchdog behave **exactly** as before — byte-for-byte
30//!   identical.
31//! * **On `wasm32`** it is a monotonic counter backed by an `AtomicU64` (no
32//!   `unsafe`, no external dependency, no wasm import). Timing there is a
33//!   meaningless-but-nonpanicking tick count. The demo masks per-decl `micros`
34//!   anyway (timing is explicitly non-deterministic, brief handoff invariant),
35//!   and the reader's node-count materialization budget — which is **not**
36//!   time-based — remains the real resource bound in the browser.
37//!
38//! Zero external dependencies; `#![forbid(unsafe_code)]` is preserved by the
39//! whole kernel.
40
41use core::ops::{Add, Sub};
42use core::time::Duration;
43
44/// A monotonic instant. See the module docs: a transparent wrapper over the
45/// standard `Instant` off wasm, and a non-panicking monotonic counter on
46/// `wasm32`.
47#[cfg(not(target_arch = "wasm32"))]
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
49pub struct Instant(::std::time::Instant);
50
51#[cfg(not(target_arch = "wasm32"))]
52impl Add<Duration> for Instant {
53    type Output = Instant;
54    fn add(self, rhs: Duration) -> Instant {
55        Instant(self.0 + rhs)
56    }
57}
58
59#[cfg(not(target_arch = "wasm32"))]
60impl Sub<Duration> for Instant {
61    type Output = Instant;
62    fn sub(self, rhs: Duration) -> Instant {
63        Instant(self.0 - rhs)
64    }
65}
66
67#[cfg(not(target_arch = "wasm32"))]
68impl Instant {
69    /// The current instant. Delegates to the standard library `Instant::now`.
70    #[must_use]
71    pub fn now() -> Self {
72        Instant(::std::time::Instant::now())
73    }
74
75    /// Time elapsed since this instant. Delegates to the standard library
76    /// `Instant::elapsed`.
77    #[must_use]
78    pub fn elapsed(&self) -> Duration {
79        self.0.elapsed()
80    }
81
82    /// Duration from `earlier` to `self`. Delegates to the standard library
83    /// `Instant::duration_since` (which saturates to zero if `earlier` is later,
84    /// matching std's documented behavior).
85    #[must_use]
86    pub fn duration_since(&self, earlier: Instant) -> Duration {
87        self.0.duration_since(earlier.0)
88    }
89}
90
91// ── wasm32: a monotonic tick counter (std time panics here) ──────────────────
92
93#[cfg(target_arch = "wasm32")]
94use core::sync::atomic::{AtomicU64, Ordering};
95
96/// A global monotonic tick source for wasm. Every [`Instant::now`](crate::wall_clock::Instant::now) bumps it by
97/// one, so instants are strictly ordered and `duration_since` never underflows.
98/// The unit is "ticks", reported as nanoseconds so `Duration::as_micros` /
99/// `as_millis` stay well-defined (they collapse to ~0, which is correct: the
100/// demo does not rely on wasm wall time).
101#[cfg(target_arch = "wasm32")]
102static TICKS: AtomicU64 = AtomicU64::new(0);
103
104/// A monotonic instant on wasm: a snapshot of the global tick counter.
105#[cfg(target_arch = "wasm32")]
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
107pub struct Instant(u64);
108
109#[cfg(target_arch = "wasm32")]
110impl Add<Duration> for Instant {
111    type Output = Instant;
112    fn add(self, rhs: Duration) -> Instant {
113        // Scale the added duration back to ticks (1 tick == 1 ns here) and
114        // saturate so deadline arithmetic never wraps.
115        Instant(self.0.saturating_add(rhs.as_nanos() as u64))
116    }
117}
118
119#[cfg(target_arch = "wasm32")]
120impl Sub<Duration> for Instant {
121    type Output = Instant;
122    fn sub(self, rhs: Duration) -> Instant {
123        // Saturate at the epoch so a "cutoff = now - window" never underflows.
124        Instant(self.0.saturating_sub(rhs.as_nanos() as u64))
125    }
126}
127
128#[cfg(target_arch = "wasm32")]
129impl Instant {
130    /// The current instant: bump and read the global monotonic tick counter.
131    #[must_use]
132    pub fn now() -> Self {
133        // Relaxed is sufficient: we only need monotonicity of the returned
134        // values, not cross-thread happens-before (wasm is single-threaded here).
135        Instant(TICKS.fetch_add(1, Ordering::Relaxed))
136    }
137
138    /// Ticks elapsed since this instant, as a `Duration` (nanosecond-scaled).
139    #[must_use]
140    pub fn elapsed(&self) -> Duration {
141        Instant::now().duration_since(*self)
142    }
143
144    /// Duration from `earlier` to `self`, saturating at zero (never underflows).
145    #[must_use]
146    pub fn duration_since(&self, earlier: Instant) -> Duration {
147        Duration::from_nanos(self.0.saturating_sub(earlier.0))
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn now_is_monotonic_and_elapsed_is_nonnegative() {
157        let a = Instant::now();
158        let b = Instant::now();
159        // duration_since never panics and is >= 0 by construction.
160        let _d = b.duration_since(a);
161        let _e = a.elapsed();
162    }
163}