Skip to main content

clock_kit/
timestamp.rs

1//! Wall/monotonic timestamp pair — REQ-CLK-002.
2//!
3//! Wall time and monotonic time are separate fields with separate rules:
4//! wall time is comparable across processes and machines and *may step*
5//! (NTP slew, PTP step, operator action); monotonic time is comparable only
6//! within a process and *never steps*. No API derives one from the other
7//! except [`Timestamp::mono_from_wall_lossy`], which is explicitly labelled
8//! lossy.
9
10#[cfg(feature = "std")]
11use core::time::Duration;
12
13/// Wall-clock instant: nanoseconds since the Unix epoch.
14///
15/// `SystemTime`-backed under `std`; a `u64` epoch-nanos wrapper under
16/// `core` + `libc` (spec §Decisions #2). The portable accessor is
17/// [`unix_nanos`](WallTime::unix_nanos) — identical on both builds.
18/// Pre-epoch readings clamp to `0`: a clock library must not panic.
19///
20/// REQ-CLK-002.
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct WallTime(u64);
23
24impl WallTime {
25    /// The Unix epoch (0 nanos).
26    pub const UNIX_EPOCH: Self = Self(0);
27
28    /// Wrap raw epoch nanos.
29    #[must_use]
30    pub const fn from_unix_nanos(ns: u64) -> Self {
31        Self(ns)
32    }
33
34    /// Nanoseconds since the Unix epoch — the identical accessor on every
35    /// build (std, libc, core-only).
36    ///
37    /// REQ-CLK-002.
38    #[must_use]
39    pub const fn unix_nanos(&self) -> u64 {
40        self.0
41    }
42
43    /// Reconstruct as `std::time::SystemTime` (std builds only).
44    ///
45    /// Saturation: absurd values beyond `SystemTime`'s range collapse to
46    /// `UNIX_EPOCH` rather than panicking.
47    #[cfg(feature = "std")]
48    #[must_use]
49    pub fn system(&self) -> std::time::SystemTime {
50        std::time::SystemTime::UNIX_EPOCH
51            .checked_add(Duration::from_nanos(self.0))
52            .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
53    }
54
55    /// Capture the current wall clock (std builds only). Pre-epoch system
56    /// clocks clamp to `0`.
57    #[cfg(feature = "std")]
58    #[must_use]
59    pub fn from_system_now() -> Self {
60        Self(now_wall_nanos())
61    }
62}
63
64/// A paired wall+monotonic reading.
65///
66/// - `wall`: see [`WallTime`] — may step, comparable across machines.
67/// - `mono`: nanos on a process-local monotonic scale — never steps and,
68///   by REQ-CLK-001, never regresses across calls within a process.
69///
70/// No `From`/`Into` conversion exists between the two fields in either
71/// direction; [`Timestamp::mono_from_wall_lossy`] is the single, explicitly
72/// lossy escape hatch. REQ-CLK-002.
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74pub struct Timestamp {
75    /// Wall time — may step, cross-process/machine comparable.
76    pub wall: WallTime,
77    /// Monotonic nanos — never regresses within a process (REQ-CLK-001).
78    pub mono: u64,
79}
80
81impl Timestamp {
82    /// Assemble a timestamp from its parts.
83    #[must_use]
84    pub const fn from_parts(wall: WallTime, mono: u64) -> Self {
85        Self { wall, mono }
86    }
87
88    /// Derive a monotonic-style value from the wall field.
89    ///
90    /// **Explicitly lossy and labelled as such** (REQ-CLK-002): wall time
91    /// steps, so the result may go backward; it lives on the wall scale,
92    /// not the process-local monotonic scale. This is the *only*
93    /// cross-derivation in the API surface.
94    #[must_use]
95    pub fn mono_from_wall_lossy(&self) -> u64 {
96        self.wall.unix_nanos()
97    }
98}
99
100#[cfg(feature = "std")]
101fn now_wall_nanos() -> u64 {
102    use std::time::{SystemTime, UNIX_EPOCH};
103    match SystemTime::now().duration_since(UNIX_EPOCH) {
104        Ok(d) => u64::try_from(d.as_nanos()).unwrap_or(u64::MAX),
105        // Pre-epoch (misconfigured machine): clamp, never panic.
106        Err(_) => 0,
107    }
108}