Skip to main content

libpetri_core/
transition.rs

1use std::collections::HashSet;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use crate::action::{BoxedAction, passthrough};
6use crate::arc::{Inhibitor, Read, Reset};
7use crate::input::In;
8use crate::output::{Out, all_places, find_forward_inputs, find_timeout};
9use crate::place::PlaceRef;
10use crate::timing::{Timing, immediate};
11
12/// Unique identifier for a transition instance.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct TransitionId(u64);
15
16static NEXT_ID: AtomicU64 = AtomicU64::new(0);
17
18impl TransitionId {
19    fn next() -> Self {
20        Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
21    }
22}
23
24/// A transition in the Time Petri Net that transforms tokens.
25///
26/// Transitions use identity-based equality (TransitionId) — each instance is unique
27/// regardless of name. The name is purely a label for display/debugging/export.
28#[derive(Clone)]
29pub struct Transition {
30    id: TransitionId,
31    name: Arc<str>,
32    input_specs: Vec<In>,
33    output_spec: Option<Out>,
34    inhibitors: Vec<Inhibitor>,
35    reads: Vec<Read>,
36    resets: Vec<Reset>,
37    timing: Timing,
38    action_timeout: Option<u64>,
39    action: BoxedAction,
40    priority: i32,
41    input_places: HashSet<PlaceRef>,
42    read_places: HashSet<PlaceRef>,
43    output_places: HashSet<PlaceRef>,
44}
45
46impl Transition {
47    /// Returns the unique transition ID.
48    pub fn id(&self) -> TransitionId {
49        self.id
50    }
51
52    /// Returns the transition name.
53    pub fn name(&self) -> &str {
54        &self.name
55    }
56
57    /// Returns the name as Arc<str>.
58    pub fn name_arc(&self) -> &Arc<str> {
59        &self.name
60    }
61
62    /// Returns the input specifications.
63    pub fn input_specs(&self) -> &[In] {
64        &self.input_specs
65    }
66
67    /// Returns the output specification, if any.
68    pub fn output_spec(&self) -> Option<&Out> {
69        self.output_spec.as_ref()
70    }
71
72    /// Returns the inhibitor arcs.
73    pub fn inhibitors(&self) -> &[Inhibitor] {
74        &self.inhibitors
75    }
76
77    /// Returns the read arcs.
78    pub fn reads(&self) -> &[Read] {
79        &self.reads
80    }
81
82    /// Returns the reset arcs.
83    pub fn resets(&self) -> &[Reset] {
84        &self.resets
85    }
86
87    /// Returns the timing specification.
88    pub fn timing(&self) -> &Timing {
89        &self.timing
90    }
91
92    /// Returns the action timeout in ms, if any.
93    pub fn action_timeout(&self) -> Option<u64> {
94        self.action_timeout
95    }
96
97    /// Returns true if this transition has an action timeout.
98    pub fn has_action_timeout(&self) -> bool {
99        self.action_timeout.is_some()
100    }
101
102    /// Returns the transition action.
103    pub fn action(&self) -> &BoxedAction {
104        &self.action
105    }
106
107    /// Returns the priority (higher fires first).
108    pub fn priority(&self) -> i32 {
109        self.priority
110    }
111
112    /// Returns set of input place refs (consumed tokens).
113    pub fn input_places(&self) -> &HashSet<PlaceRef> {
114        &self.input_places
115    }
116
117    /// Returns set of read place refs (context tokens, not consumed).
118    pub fn read_places(&self) -> &HashSet<PlaceRef> {
119        &self.read_places
120    }
121
122    /// Returns set of output place refs (where tokens are produced).
123    pub fn output_places(&self) -> &HashSet<PlaceRef> {
124        &self.output_places
125    }
126
127    /// Creates a new TransitionBuilder.
128    pub fn builder(name: impl Into<Arc<str>>) -> TransitionBuilder {
129        TransitionBuilder::new(name)
130    }
131}
132
133impl std::fmt::Debug for Transition {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        f.debug_struct("Transition")
136            .field("id", &self.id)
137            .field("name", &self.name)
138            .field("timing", &self.timing)
139            .field("priority", &self.priority)
140            .finish()
141    }
142}
143
144impl PartialEq for Transition {
145    fn eq(&self, other: &Self) -> bool {
146        self.id == other.id
147    }
148}
149
150impl Eq for Transition {}
151
152impl std::hash::Hash for Transition {
153    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
154        self.id.hash(state);
155    }
156}
157
158impl std::fmt::Display for Transition {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        write!(f, "Transition[{}]", self.name)
161    }
162}
163
164/// Builder for constructing Transition instances.
165pub struct TransitionBuilder {
166    name: Arc<str>,
167    input_specs: Vec<In>,
168    output_spec: Option<Out>,
169    inhibitors: Vec<Inhibitor>,
170    reads: Vec<Read>,
171    resets: Vec<Reset>,
172    timing: Timing,
173    action: BoxedAction,
174    priority: i32,
175}
176
177impl TransitionBuilder {
178    pub fn new(name: impl Into<Arc<str>>) -> Self {
179        Self {
180            name: name.into(),
181            input_specs: Vec::new(),
182            output_spec: None,
183            inhibitors: Vec::new(),
184            reads: Vec::new(),
185            resets: Vec::new(),
186            timing: immediate(),
187            action: passthrough(),
188            priority: 0,
189        }
190    }
191
192    /// Add a single input specification.
193    pub fn input(mut self, spec: In) -> Self {
194        self.input_specs.push(spec);
195        self
196    }
197
198    /// Add multiple input specifications.
199    pub fn inputs(mut self, specs: Vec<In>) -> Self {
200        self.input_specs.extend(specs);
201        self
202    }
203
204    /// Set the output specification.
205    pub fn output(mut self, spec: Out) -> Self {
206        self.output_spec = Some(spec);
207        self
208    }
209
210    /// Add an inhibitor arc.
211    pub fn inhibitor(mut self, inh: Inhibitor) -> Self {
212        self.inhibitors.push(inh);
213        self
214    }
215
216    /// Add multiple inhibitor arcs.
217    pub fn inhibitors(mut self, inhs: Vec<Inhibitor>) -> Self {
218        self.inhibitors.extend(inhs);
219        self
220    }
221
222    /// Add a read arc.
223    pub fn read(mut self, r: Read) -> Self {
224        self.reads.push(r);
225        self
226    }
227
228    /// Add multiple read arcs.
229    pub fn reads(mut self, rs: Vec<Read>) -> Self {
230        self.reads.extend(rs);
231        self
232    }
233
234    /// Add a reset arc.
235    pub fn reset(mut self, r: Reset) -> Self {
236        self.resets.push(r);
237        self
238    }
239
240    /// Add multiple reset arcs.
241    pub fn resets(mut self, rs: Vec<Reset>) -> Self {
242        self.resets.extend(rs);
243        self
244    }
245
246    /// Set timing specification.
247    pub fn timing(mut self, timing: Timing) -> Self {
248        self.timing = timing;
249        self
250    }
251
252    /// Set the transition action.
253    pub fn action(mut self, action: BoxedAction) -> Self {
254        self.action = action;
255        self
256    }
257
258    /// Set priority (higher fires first).
259    pub fn priority(mut self, priority: i32) -> Self {
260        self.priority = priority;
261        self
262    }
263
264    /// Build the transition.
265    ///
266    /// # Panics
267    /// Panics if ForwardInput references a non-input place.
268    pub fn build(self) -> Transition {
269        // Validate ForwardInput references
270        if let Some(ref out) = self.output_spec {
271            let input_place_names: HashSet<_> =
272                self.input_specs.iter().map(|s| s.place_name()).collect();
273            for (from, _) in find_forward_inputs(out) {
274                assert!(
275                    input_place_names.contains(from.name()),
276                    "Transition '{}': ForwardInput references non-input place '{}'",
277                    self.name,
278                    from.name()
279                );
280            }
281        }
282
283        let action_timeout = self
284            .output_spec
285            .as_ref()
286            .and_then(|o| find_timeout(o).map(|(ms, _)| ms));
287
288        // Precompute place sets
289        let input_places: HashSet<PlaceRef> =
290            self.input_specs.iter().map(|s| s.place().clone()).collect();
291
292        let read_places: HashSet<PlaceRef> = self.reads.iter().map(|r| r.place.clone()).collect();
293
294        let output_places: HashSet<PlaceRef> = self
295            .output_spec
296            .as_ref()
297            .map(all_places)
298            .unwrap_or_default();
299
300        Transition {
301            id: TransitionId::next(),
302            name: self.name,
303            input_specs: self.input_specs,
304            output_spec: self.output_spec,
305            inhibitors: self.inhibitors,
306            reads: self.reads,
307            resets: self.resets,
308            timing: self.timing,
309            action_timeout,
310            action: self.action,
311            priority: self.priority,
312            input_places,
313            read_places,
314            output_places,
315        }
316    }
317}
318
319/// Creates a new transition with a different action while preserving all arc specs.
320pub(crate) fn rebuild_with_action(t: &Transition, action: BoxedAction) -> Transition {
321    let mut builder = Transition::builder(Arc::clone(&t.name))
322        .timing(t.timing)
323        .priority(t.priority)
324        .action(action)
325        .inputs(t.input_specs.clone())
326        .inhibitors(t.inhibitors.clone())
327        .reads(t.reads.clone())
328        .resets(t.resets.clone());
329
330    if let Some(ref out) = t.output_spec {
331        builder = builder.output(out.clone());
332    }
333
334    builder.build()
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::input::one;
341    use crate::output::out_place;
342    use crate::place::Place;
343
344    #[test]
345    fn transition_builder_basic() {
346        let p_in = Place::<i32>::new("in");
347        let p_out = Place::<i32>::new("out");
348
349        let t = Transition::builder("test")
350            .input(one(&p_in))
351            .output(out_place(&p_out))
352            .build();
353
354        assert_eq!(t.name(), "test");
355        assert_eq!(t.input_specs().len(), 1);
356        assert!(t.output_spec().is_some());
357        assert_eq!(t.timing(), &Timing::Immediate);
358        assert_eq!(t.priority(), 0);
359    }
360
361    #[test]
362    fn transition_identity() {
363        let t1 = Transition::builder("test").build();
364        let t2 = Transition::builder("test").build();
365        assert_ne!(t1, t2); // different IDs
366    }
367
368    #[test]
369    fn transition_places_computed() {
370        let p_in = Place::<i32>::new("in");
371        let p_out = Place::<i32>::new("out");
372        let p_read = Place::<String>::new("ctx");
373
374        let t = Transition::builder("test")
375            .input(one(&p_in))
376            .output(out_place(&p_out))
377            .read(crate::arc::read(&p_read))
378            .build();
379
380        assert!(t.input_places().contains(&PlaceRef::new("in")));
381        assert!(t.output_places().contains(&PlaceRef::new("out")));
382        assert!(t.read_places().contains(&PlaceRef::new("ctx")));
383    }
384
385    #[test]
386    #[should_panic(expected = "ForwardInput references non-input place")]
387    fn forward_input_validation() {
388        let from = Place::<i32>::new("not-an-input");
389        let to = Place::<i32>::new("to");
390
391        Transition::builder("test")
392            .output(crate::output::forward_input(&from, &to))
393            .build();
394    }
395
396    #[test]
397    fn action_timeout_detected() {
398        let p = Place::<i32>::new("timeout");
399        let t = Transition::builder("test")
400            .output(crate::output::timeout_place(5000, &p))
401            .build();
402        assert_eq!(t.action_timeout(), Some(5000));
403    }
404
405    #[test]
406    fn no_action_timeout() {
407        let p = Place::<i32>::new("out");
408        let t = Transition::builder("test").output(out_place(&p)).build();
409        assert_eq!(t.action_timeout(), None);
410    }
411}