Skip to main content

libpetri_core/
timing.rs

1/// ~100 years in milliseconds, used for "unconstrained" intervals.
2pub const MAX_DURATION_MS: u64 = 365 * 100 * 24 * 60 * 60 * 1000;
3
4/// Firing timing specification for transitions.
5///
6/// Based on classical Time Petri Net (TPN) semantics:
7/// - Transition CANNOT fire before earliest time (lower bound)
8/// - Transition MUST fire by deadline OR become disabled (upper bound)
9///
10/// All durations are in milliseconds.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Timing {
13    /// Can fire as soon as enabled, no deadline. [0, inf)
14    Immediate,
15    /// Can fire immediately, must fire by deadline. [0, by]
16    Deadline { by_ms: u64 },
17    /// Must wait, then can fire anytime. [after, inf)
18    Delayed { after_ms: u64 },
19    /// Can fire within [earliest, latest].
20    Window { earliest_ms: u64, latest_ms: u64 },
21    /// Fires at precisely the specified time. [at, at]
22    Exact { at_ms: u64 },
23}
24
25// ==================== Factory Functions ====================
26
27/// Immediate firing: can fire as soon as enabled, no deadline.
28pub fn immediate() -> Timing {
29    Timing::Immediate
30}
31
32/// Immediate with deadline: can fire immediately, must fire by deadline.
33///
34/// # Panics
35/// Panics if `by_ms` is 0.
36pub fn deadline(by_ms: u64) -> Timing {
37    assert!(by_ms > 0, "Deadline must be positive: {by_ms}");
38    Timing::Deadline { by_ms }
39}
40
41/// Delayed firing: must wait, then can fire anytime.
42pub fn delayed(after_ms: u64) -> Timing {
43    Timing::Delayed { after_ms }
44}
45
46/// Time window: can fire within [earliest, latest].
47///
48/// # Panics
49/// Panics if `latest_ms < earliest_ms`.
50pub fn window(earliest_ms: u64, latest_ms: u64) -> Timing {
51    assert!(
52        latest_ms >= earliest_ms,
53        "Latest ({latest_ms}) must be >= earliest ({earliest_ms})"
54    );
55    Timing::Window {
56        earliest_ms,
57        latest_ms,
58    }
59}
60
61/// Exact timing: fires at precisely the specified time.
62pub fn exact(at_ms: u64) -> Timing {
63    Timing::Exact { at_ms }
64}
65
66// ==================== Query Functions ====================
67
68impl Timing {
69    /// Returns the earliest time (ms) the transition can fire after enabling.
70    pub fn earliest(&self) -> u64 {
71        match self {
72            Timing::Immediate => 0,
73            Timing::Deadline { .. } => 0,
74            Timing::Delayed { after_ms } => *after_ms,
75            Timing::Window { earliest_ms, .. } => *earliest_ms,
76            Timing::Exact { at_ms } => *at_ms,
77        }
78    }
79
80    /// Returns the latest time (ms) by which the transition must fire.
81    pub fn latest(&self) -> u64 {
82        match self {
83            Timing::Immediate => MAX_DURATION_MS,
84            Timing::Deadline { by_ms } => *by_ms,
85            Timing::Delayed { .. } => MAX_DURATION_MS,
86            Timing::Window { latest_ms, .. } => *latest_ms,
87            Timing::Exact { at_ms } => *at_ms,
88        }
89    }
90
91    /// Returns true if this timing has a finite deadline.
92    pub fn has_deadline(&self) -> bool {
93        matches!(
94            self,
95            Timing::Deadline { .. } | Timing::Window { .. } | Timing::Exact { .. }
96        )
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn immediate_bounds() {
106        let t = immediate();
107        assert_eq!(t.earliest(), 0);
108        assert_eq!(t.latest(), MAX_DURATION_MS);
109        assert!(!t.has_deadline());
110    }
111
112    #[test]
113    fn deadline_bounds() {
114        let t = deadline(5000);
115        assert_eq!(t.earliest(), 0);
116        assert_eq!(t.latest(), 5000);
117        assert!(t.has_deadline());
118    }
119
120    #[test]
121    fn delayed_bounds() {
122        let t = delayed(100);
123        assert_eq!(t.earliest(), 100);
124        assert_eq!(t.latest(), MAX_DURATION_MS);
125        assert!(!t.has_deadline());
126    }
127
128    #[test]
129    fn window_bounds() {
130        let t = window(100, 500);
131        assert_eq!(t.earliest(), 100);
132        assert_eq!(t.latest(), 500);
133        assert!(t.has_deadline());
134    }
135
136    #[test]
137    fn exact_bounds() {
138        let t = exact(250);
139        assert_eq!(t.earliest(), 250);
140        assert_eq!(t.latest(), 250);
141        assert!(t.has_deadline());
142    }
143
144    #[test]
145    #[should_panic(expected = "Deadline must be positive")]
146    fn deadline_zero_panics() {
147        deadline(0);
148    }
149
150    #[test]
151    #[should_panic(expected = "Latest")]
152    fn window_inverted_panics() {
153        window(500, 100);
154    }
155}