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}