qubit_clock/monotonic/monotonic_instant.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Defines a monotonic instant scoped to one clock domain.
9
10use std::cmp::Ordering;
11use std::time::Duration;
12
13use crate::ClockDomain;
14use crate::TimeError;
15
16/// A fixed point in one monotonic clock domain.
17///
18/// Instants from different domains cannot be ordered or used in arithmetic
19/// together. The value carries the full precision available through
20/// [`Duration`] without claiming any particular hardware timer resolution.
21///
22/// Discarding a sampled instant is rejected when `unused_must_use` is denied:
23///
24/// ```compile_fail
25/// #![deny(unused_must_use)]
26/// use qubit_clock::{ManualMonotonicClock, MonotonicClock};
27///
28/// ManualMonotonicClock::new().now();
29/// ```
30#[must_use = "monotonic instants should be used to measure or compare time"]
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct MonotonicInstant {
33 /// The identifier of the originating monotonic clock domain.
34 domain: ClockDomain,
35 /// The elapsed duration from the originating clock domain's origin.
36 elapsed: Duration,
37}
38
39impl MonotonicInstant {
40 /// Creates an instant for a clock implementation.
41 ///
42 /// # Parameters
43 ///
44 /// * `domain` - Identifier of the originating clock.
45 /// * `elapsed` - Duration measured from that clock's private origin.
46 ///
47 /// # Returns
48 ///
49 /// An instant scoped to `domain` at `elapsed`.
50 #[inline(always)]
51 pub const fn new(domain: ClockDomain, elapsed: Duration) -> Self {
52 Self { domain, elapsed }
53 }
54
55 /// Returns the identifier of the originating monotonic clock domain.
56 ///
57 /// # Returns
58 ///
59 /// The domain carried by this instant.
60 #[inline(always)]
61 pub const fn domain(self) -> ClockDomain {
62 self.domain
63 }
64
65 /// Returns the elapsed duration from this clock domain's origin.
66 ///
67 /// The value is meaningful only inside the domain identified by
68 /// [`domain()`](Self::domain).
69 ///
70 /// # Returns
71 ///
72 /// The duration from the originating clock's private origin.
73 #[must_use]
74 #[inline(always)]
75 pub const fn elapsed_since_origin(self) -> Duration {
76 self.elapsed
77 }
78
79 /// Adds a duration while preserving the originating clock domain.
80 ///
81 /// Returns [`TimeError::InstantOverflow`] when the result cannot be
82 /// represented by [`Duration`].
83 ///
84 /// # Parameters
85 ///
86 /// * `duration` - Duration to add to this instant.
87 ///
88 /// # Returns
89 ///
90 /// A same-domain instant advanced by `duration`.
91 ///
92 /// # Errors
93 ///
94 /// Returns [`TimeError::InstantOverflow`] when the elapsed duration
95 /// cannot represent the result.
96 #[inline]
97 pub fn checked_add(self, duration: Duration) -> Result<Self, TimeError> {
98 let elapsed = self.elapsed.checked_add(duration).ok_or(TimeError::InstantOverflow)?;
99 Ok(Self::new(self.domain, elapsed))
100 }
101
102 /// Calculates the duration elapsed since an earlier instant.
103 ///
104 /// Returns [`TimeError::ClockDomainMismatch`] when `earlier` belongs to a
105 /// different clock, and [`TimeError::InvalidInstantOrder`] when `earlier`
106 /// is later than this instant.
107 ///
108 /// # Parameters
109 ///
110 /// * `earlier` - Earlier instant expected to belong to the same domain.
111 ///
112 /// # Returns
113 ///
114 /// The elapsed duration from `earlier` to this instant.
115 ///
116 /// # Errors
117 ///
118 /// Returns [`TimeError::ClockDomainMismatch`] for a foreign instant.
119 /// Returns [`TimeError::InvalidInstantOrder`] when `earlier` is later,
120 /// retaining both elapsed durations.
121 #[inline]
122 pub fn duration_since(self, earlier: Self) -> Result<Duration, TimeError> {
123 earlier.validate_domain(self.domain)?;
124 self.elapsed
125 .checked_sub(earlier.elapsed)
126 .ok_or(TimeError::InvalidInstantOrder {
127 current_elapsed: self.elapsed,
128 earlier_elapsed: earlier.elapsed,
129 })
130 }
131
132 /// Verifies that this instant belongs to `expected_domain`.
133 ///
134 /// Custom monotonic clocks and timers can use this method to reject
135 /// externally supplied instants before performing domain-specific work.
136 ///
137 /// # Parameters
138 ///
139 /// * `expected_domain` - Domain the instant must belong to.
140 ///
141 /// # Returns
142 ///
143 /// `Ok(())` when the domains match.
144 ///
145 /// # Errors
146 ///
147 /// Returns [`TimeError::ClockDomainMismatch`] when the domains differ.
148 #[inline]
149 pub fn validate_domain(self, expected_domain: ClockDomain) -> Result<(), TimeError> {
150 if self.domain == expected_domain {
151 Ok(())
152 } else {
153 Err(TimeError::ClockDomainMismatch {
154 expected: expected_domain,
155 actual: self.domain,
156 })
157 }
158 }
159}
160
161impl PartialOrd for MonotonicInstant {
162 /// Orders two instants only when they belong to the same clock domain.
163 ///
164 /// # Parameters
165 ///
166 /// * `other` - Instant to compare with this one.
167 ///
168 /// # Returns
169 ///
170 /// Their elapsed-time ordering for a shared domain, or `None` for distinct
171 /// domains.
172 #[inline]
173 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
174 (self.domain == other.domain).then(|| self.elapsed.cmp(&other.elapsed))
175 }
176}