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}