Skip to main content

libpetri_runtime/
precompiled_net.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use libpetri_core::input::In;
5use libpetri_core::output::Out;
6use libpetri_core::petri_net::PetriNet;
7use libpetri_core::place::PlaceRef;
8use libpetri_core::transition::Transition;
9
10use crate::bitmap;
11use crate::compiled_net::CompiledNet;
12
13/// Consume operation opcodes.
14pub const CONSUME_ONE: u32 = 0;
15pub const CONSUME_N: u32 = 1;
16pub const CONSUME_ALL: u32 = 2;
17pub const CONSUME_ATLEAST: u32 = 3;
18pub const RESET: u32 = 4;
19
20/// Sparse enablement mask: precomputed for efficient bitmap checks.
21#[derive(Debug, Clone)]
22pub enum SparseMask {
23    /// No bits set (e.g. no inputs / no inhibitors).
24    Empty,
25    /// Exactly one non-zero word at the given index.
26    Single { word_index: usize, mask: u64 },
27    /// Multiple non-zero words (sparse representation).
28    Multi {
29        indices: Vec<usize>,
30        masks: Vec<u64>,
31    },
32}
33
34/// Precompiled flat-array net representation for the [`super::precompiled_executor::PrecompiledNetExecutor`].
35///
36/// Compiles the net topology into flat arrays and operation sequences that eliminate
37/// HashMap lookups and enum pattern matching from the hot path.
38#[derive(Debug)]
39pub struct PrecompiledNet<'c> {
40    compiled: &'c CompiledNet,
41
42    /// Consume/reset opcodes per transition.
43    pub(crate) consume_ops: Vec<Vec<u32>>,
44    /// Read-arc place IDs per transition.
45    pub(crate) read_ops: Vec<Vec<usize>>,
46
47    /// Sparse enablement masks per transition.
48    pub(crate) needs_sparse: Vec<SparseMask>,
49    pub(crate) inhibitor_sparse: Vec<SparseMask>,
50
51    /// Precomputed timing (milliseconds).
52    pub(crate) earliest_ms: Vec<f64>,
53    pub(crate) latest_ms: Vec<f64>,
54    pub(crate) has_deadline: Vec<bool>,
55    pub(crate) all_immediate: bool,
56    pub(crate) any_deadlines: bool,
57
58    /// Priority partitioning.
59    pub(crate) priorities: Vec<i32>,
60    pub(crate) transition_to_priority_index: Vec<usize>,
61    #[allow(dead_code)]
62    pub(crate) priority_levels: Vec<i32>,
63    pub(crate) distinct_priority_count: usize,
64    pub(crate) all_same_priority: bool,
65
66    /// Simple output fast path: -2 = no spec, -1 = complex, >= 0 = Out::Place pid.
67    #[allow(dead_code)]
68    pub(crate) simple_output_place_id: Vec<i32>,
69
70    /// Input place count per transition (for capacity hints).
71    #[allow(dead_code)]
72    pub(crate) input_place_count: Vec<usize>,
73
74    /// Input place masks per transition (for reset-clock detection).
75    pub(crate) input_place_mask_words: Vec<Vec<u64>>,
76
77    /// Precomputed place name Arcs (indexed by place ID).
78    pub(crate) place_name_arcs: Vec<Arc<str>>,
79    /// Precomputed transition name Arcs (indexed by transition ID).
80    pub(crate) transition_name_arcs: Vec<Arc<str>>,
81    /// Precomputed output place name sets per transition.
82    pub(crate) output_place_name_sets: Vec<HashSet<Arc<str>>>,
83}
84
85impl<'c> PrecompiledNet<'c> {
86    /// Compiles using an existing CompiledNet to reuse its masks and indices.
87    pub fn from_compiled(compiled: &'c CompiledNet) -> Self {
88        let tc = compiled.transition_count;
89        let wc = compiled.word_count;
90
91        // Compile sparse enablement masks
92        let mut needs_sparse = Vec::with_capacity(tc);
93        let mut inhibitor_sparse = Vec::with_capacity(tc);
94
95        for tid in 0..tc {
96            needs_sparse.push(compile_sparse(compiled.needs_mask(tid)));
97            inhibitor_sparse.push(compile_sparse(compiled.inhibitor_mask(tid)));
98        }
99
100        // Compile consume/read operation sequences
101        let mut consume_ops = Vec::with_capacity(tc);
102        let mut read_ops = Vec::with_capacity(tc);
103        let mut input_place_mask_words = Vec::with_capacity(tc);
104        let mut simple_output_place_id = Vec::with_capacity(tc);
105        let mut input_place_count = Vec::with_capacity(tc);
106
107        for tid in 0..tc {
108            let t = compiled.transition(tid);
109            consume_ops.push(compile_consume_ops(t, compiled));
110            read_ops.push(compile_read_ops(t, compiled));
111            input_place_mask_words.push(compile_input_mask(t, compiled, wc));
112
113            input_place_count.push(t.input_specs().len() + t.reads().len());
114
115            // Precompute output validation fast path
116            match t.output_spec() {
117                None => simple_output_place_id.push(-2),
118                Some(Out::Place(p)) => {
119                    simple_output_place_id.push(compiled.place_id(p.name()).unwrap_or(0) as i32);
120                }
121                Some(_) => simple_output_place_id.push(-1),
122            }
123        }
124
125        // Precompute timing
126        let mut earliest_ms = vec![0.0f64; tc];
127        let mut latest_ms = vec![f64::MAX; tc];
128        let mut has_deadline = vec![false; tc];
129        let mut any_deadlines = false;
130        let mut all_immediate = true;
131
132        for tid in 0..tc {
133            let t = compiled.transition(tid);
134            earliest_ms[tid] = t.timing().earliest() as f64;
135            if t.timing().has_deadline() {
136                latest_ms[tid] = t.timing().latest() as f64;
137                has_deadline[tid] = true;
138                any_deadlines = true;
139            }
140            if *t.timing() != libpetri_core::timing::Timing::Immediate {
141                all_immediate = false;
142            }
143        }
144
145        // Precompute priorities
146        let mut priorities = vec![0i32; tc];
147        let mut distinct_set = std::collections::BTreeSet::new();
148        for (tid, prio) in priorities.iter_mut().enumerate() {
149            *prio = compiled.transition(tid).priority();
150            distinct_set.insert(std::cmp::Reverse(*prio));
151        }
152        let priority_levels: Vec<i32> = distinct_set.into_iter().map(|r| r.0).collect();
153        let distinct_priority_count = priority_levels.len();
154        let all_same_priority = distinct_priority_count <= 1;
155
156        let prio_to_index: HashMap<i32, usize> = priority_levels
157            .iter()
158            .enumerate()
159            .map(|(i, &p)| (p, i))
160            .collect();
161        let transition_to_priority_index: Vec<usize> =
162            (0..tc).map(|tid| prio_to_index[&priorities[tid]]).collect();
163
164        // Precompute name Arcs for zero-allocation hot path access
165        let pc = compiled.place_count;
166        let place_name_arcs: Vec<Arc<str>> = (0..pc)
167            .map(|pid| Arc::clone(compiled.place(pid).name_arc()))
168            .collect();
169        let transition_name_arcs: Vec<Arc<str>> = (0..tc)
170            .map(|tid| Arc::clone(compiled.transition(tid).name_arc()))
171            .collect();
172
173        // Precompute output place name sets per transition
174        let output_place_name_sets: Vec<HashSet<Arc<str>>> = (0..tc)
175            .map(|tid| {
176                compiled
177                    .transition(tid)
178                    .output_places()
179                    .iter()
180                    .map(|p| Arc::clone(p.name_arc()))
181                    .collect()
182            })
183            .collect();
184
185        PrecompiledNet {
186            compiled,
187            consume_ops,
188            read_ops,
189            needs_sparse,
190            inhibitor_sparse,
191            earliest_ms,
192            latest_ms,
193            has_deadline,
194            all_immediate,
195            any_deadlines,
196            priorities,
197            transition_to_priority_index,
198            priority_levels,
199            distinct_priority_count,
200            all_same_priority,
201            simple_output_place_id,
202            input_place_count,
203            input_place_mask_words,
204            place_name_arcs,
205            transition_name_arcs,
206            output_place_name_sets,
207        }
208    }
209
210    // ==================== Accessors ====================
211
212    /// Returns the underlying CompiledNet.
213    pub fn compiled(&self) -> &'c CompiledNet {
214        self.compiled
215    }
216
217    /// Returns the underlying PetriNet.
218    pub fn net(&self) -> &PetriNet {
219        self.compiled.net()
220    }
221
222    /// Returns the place at the given ID.
223    pub fn place(&self, pid: usize) -> &PlaceRef {
224        self.compiled.place(pid)
225    }
226
227    /// Returns the transition at the given compiled ID.
228    pub fn transition(&self, tid: usize) -> &Transition {
229        self.compiled.transition(tid)
230    }
231
232    /// Returns the place ID for a given place name.
233    pub fn place_id(&self, name: &str) -> Option<usize> {
234        self.compiled.place_id(name)
235    }
236
237    /// Returns the number of places.
238    pub fn place_count(&self) -> usize {
239        self.compiled.place_count
240    }
241
242    /// Returns the number of transitions.
243    pub fn transition_count(&self) -> usize {
244        self.compiled.transition_count
245    }
246
247    /// Returns the word count for bitmaps.
248    pub fn word_count(&self) -> usize {
249        self.compiled.word_count
250    }
251
252    // ==================== Enablement Check ====================
253
254    /// Sparse bitmap enablement check for a transition.
255    pub fn can_enable_bitmap(&self, tid: usize, marking_snapshot: &[u64]) -> bool {
256        // Needs check (contains_all)
257        match &self.needs_sparse[tid] {
258            SparseMask::Empty => {}
259            SparseMask::Single { word_index, mask } => {
260                if *word_index >= marking_snapshot.len()
261                    || (marking_snapshot[*word_index] & mask) != *mask
262                {
263                    return false;
264                }
265            }
266            SparseMask::Multi { indices, masks } => {
267                for i in 0..indices.len() {
268                    let w = indices[i];
269                    if w >= marking_snapshot.len() || (marking_snapshot[w] & masks[i]) != masks[i] {
270                        return false;
271                    }
272                }
273            }
274        }
275
276        // Inhibitor check (intersects → must not intersect)
277        match &self.inhibitor_sparse[tid] {
278            SparseMask::Empty => true,
279            SparseMask::Single { word_index, mask } => {
280                *word_index >= marking_snapshot.len() || (marking_snapshot[*word_index] & mask) == 0
281            }
282            SparseMask::Multi { indices, masks } => {
283                for i in 0..indices.len() {
284                    let w = indices[i];
285                    if w < marking_snapshot.len() && (marking_snapshot[w] & masks[i]) != 0 {
286                        return false;
287                    }
288                }
289                true
290            }
291        }
292    }
293}
294
295// ==================== Compilation Helpers ====================
296
297fn compile_sparse(mask_words: &[u64]) -> SparseMask {
298    let mut non_zero_count = 0;
299    let mut last_non_zero = 0;
300    for (w, &word) in mask_words.iter().enumerate() {
301        if word != 0 {
302            non_zero_count += 1;
303            last_non_zero = w;
304        }
305    }
306
307    match non_zero_count {
308        0 => SparseMask::Empty,
309        1 => SparseMask::Single {
310            word_index: last_non_zero,
311            mask: mask_words[last_non_zero],
312        },
313        _ => {
314            let mut indices = Vec::with_capacity(non_zero_count);
315            let mut masks = Vec::with_capacity(non_zero_count);
316            for (w, &word) in mask_words.iter().enumerate() {
317                if word != 0 {
318                    indices.push(w);
319                    masks.push(word);
320                }
321            }
322            SparseMask::Multi { indices, masks }
323        }
324    }
325}
326
327fn compile_consume_ops(t: &Transition, compiled: &CompiledNet) -> Vec<u32> {
328    let mut ops = Vec::new();
329
330    for in_spec in t.input_specs() {
331        let pid = compiled.place_id(in_spec.place_name()).unwrap();
332        match in_spec {
333            In::One { .. } => {
334                ops.push(CONSUME_ONE);
335                ops.push(pid as u32);
336            }
337            In::Exactly { count, .. } => {
338                ops.push(CONSUME_N);
339                ops.push(pid as u32);
340                ops.push(*count as u32);
341            }
342            In::All { .. } => {
343                ops.push(CONSUME_ALL);
344                ops.push(pid as u32);
345            }
346            In::AtLeast { minimum, .. } => {
347                ops.push(CONSUME_ATLEAST);
348                ops.push(pid as u32);
349                ops.push(*minimum as u32);
350            }
351        }
352    }
353
354    for r in t.resets() {
355        let pid = compiled.place_id(r.place.name()).unwrap();
356        ops.push(RESET);
357        ops.push(pid as u32);
358    }
359
360    ops
361}
362
363fn compile_read_ops(t: &Transition, compiled: &CompiledNet) -> Vec<usize> {
364    t.reads()
365        .iter()
366        .map(|r| compiled.place_id(r.place.name()).unwrap())
367        .collect()
368}
369
370fn compile_input_mask(t: &Transition, compiled: &CompiledNet, word_count: usize) -> Vec<u64> {
371    let mut mask = vec![0u64; word_count];
372    for in_spec in t.input_specs() {
373        let pid = compiled.place_id(in_spec.place_name()).unwrap();
374        bitmap::set_bit(&mut mask, pid);
375    }
376    mask
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use libpetri_core::input::one;
383    use libpetri_core::output::out_place;
384    use libpetri_core::place::Place;
385
386    fn simple_chain_net() -> PetriNet {
387        let p1 = Place::<i32>::new("p1");
388        let p2 = Place::<i32>::new("p2");
389        let p3 = Place::<i32>::new("p3");
390
391        let t1 = Transition::builder("t1")
392            .input(one(&p1))
393            .output(out_place(&p2))
394            .build();
395        let t2 = Transition::builder("t2")
396            .input(one(&p2))
397            .output(out_place(&p3))
398            .build();
399
400        PetriNet::builder("chain").transitions([t1, t2]).build()
401    }
402
403    #[test]
404    fn compile_basic() {
405        let net = simple_chain_net();
406        let compiled = CompiledNet::compile(&net);
407        let prog = PrecompiledNet::from_compiled(&compiled);
408
409        assert_eq!(prog.place_count(), 3);
410        assert_eq!(prog.transition_count(), 2);
411        assert!(prog.word_count() >= 1);
412        assert!(prog.all_immediate);
413        assert!(prog.all_same_priority);
414    }
415
416    #[test]
417    fn sparse_enablement() {
418        let net = simple_chain_net();
419        let compiled = CompiledNet::compile(&net);
420        let prog = PrecompiledNet::from_compiled(&compiled);
421
422        let mut snapshot = vec![0u64; prog.word_count()];
423
424        // Nothing marked — neither enabled
425        assert!(!prog.can_enable_bitmap(0, &snapshot));
426        assert!(!prog.can_enable_bitmap(1, &snapshot));
427
428        // Mark p1 — t1 enabled
429        let p1_id = prog.place_id("p1").unwrap();
430        bitmap::set_bit(&mut snapshot, p1_id);
431        assert!(prog.can_enable_bitmap(0, &snapshot));
432        assert!(!prog.can_enable_bitmap(1, &snapshot));
433
434        // Mark p2 — t2 also enabled
435        let p2_id = prog.place_id("p2").unwrap();
436        bitmap::set_bit(&mut snapshot, p2_id);
437        assert!(prog.can_enable_bitmap(0, &snapshot));
438        assert!(prog.can_enable_bitmap(1, &snapshot));
439    }
440
441    #[test]
442    fn consume_ops_compiled() {
443        let net = simple_chain_net();
444        let compiled = CompiledNet::compile(&net);
445        let prog = PrecompiledNet::from_compiled(&compiled);
446
447        // t1: CONSUME_ONE pid(p1)
448        assert_eq!(prog.consume_ops[0].len(), 2);
449        assert_eq!(prog.consume_ops[0][0], CONSUME_ONE);
450    }
451
452    #[test]
453    fn priority_partitioning() {
454        let p = Place::<()>::new("p");
455        let out = Place::<()>::new("out");
456
457        let t_high = Transition::builder("t_high")
458            .input(one(&p))
459            .output(out_place(&out))
460            .priority(10)
461            .build();
462        let t_low = Transition::builder("t_low")
463            .input(one(&p))
464            .output(out_place(&out))
465            .priority(1)
466            .build();
467
468        let net = PetriNet::builder("prio")
469            .transitions([t_high, t_low])
470            .build();
471        let compiled = CompiledNet::compile(&net);
472        let prog = PrecompiledNet::from_compiled(&compiled);
473
474        assert_eq!(prog.distinct_priority_count, 2);
475        assert!(!prog.all_same_priority);
476        assert_eq!(prog.priority_levels[0], 10); // highest first
477        assert_eq!(prog.priority_levels[1], 1);
478    }
479
480    #[test]
481    fn inhibitor_sparse() {
482        let p1 = Place::<i32>::new("p1");
483        let p2 = Place::<i32>::new("p2");
484        let p_inh = Place::<i32>::new("inh");
485
486        let t = Transition::builder("t1")
487            .input(one(&p1))
488            .output(out_place(&p2))
489            .inhibitor(libpetri_core::arc::inhibitor(&p_inh))
490            .build();
491
492        let net = PetriNet::builder("test").transition(t).build();
493        let compiled = CompiledNet::compile(&net);
494        let prog = PrecompiledNet::from_compiled(&compiled);
495
496        let mut snapshot = vec![0u64; prog.word_count()];
497        let p1_id = prog.place_id("p1").unwrap();
498        let inh_id = prog.place_id("inh").unwrap();
499
500        bitmap::set_bit(&mut snapshot, p1_id);
501        assert!(prog.can_enable_bitmap(0, &snapshot));
502
503        bitmap::set_bit(&mut snapshot, inh_id);
504        assert!(!prog.can_enable_bitmap(0, &snapshot));
505    }
506}