Skip to main content

scan_core/program_graph/
builder.rs

1use std::{collections::BTreeMap, ops::RangeBounds};
2
3use super::*;
4use crate::grammar::{Type, Val};
5use log::info;
6
7type LocationBuilderData = (BTreeMap<Action, Vec<Transition>>, Vec<(Clock, TimeRange)>);
8
9/// Defines and builds a PG.
10#[derive(Debug, Clone)]
11pub struct ProgramGraphBuilder {
12    // initial_states: Vec<Location>,
13    initial_states: Vec<Location>,
14    // Effects are indexed by actions
15    effects: Vec<Effect>,
16    // Transitions are indexed by locations
17    // Time invariants of each location
18    locations: Vec<LocationBuilderData>,
19    // Local variables with initial value.
20    vars: Vec<Val>,
21    // Number of clocks
22    clocks: u16,
23}
24
25impl Default for ProgramGraphBuilder {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl ProgramGraphBuilder {
32    /// Creates a new [`ProgramGraphBuilder`].
33    /// At creation, this will only have the initial location with no variables, no actions and no transitions.
34    pub fn new() -> Self {
35        Self {
36            initial_states: Vec::new(),
37            effects: Vec::new(),
38            vars: Vec::new(),
39            locations: Vec::new(),
40            clocks: 0,
41        }
42    }
43
44    // Gets the type of a variable.
45    pub(crate) fn var_type(&self, var: Var) -> Result<Type, PgError> {
46        self.vars
47            .get(var.0 as usize)
48            .map(|val| Val::r#type(*val))
49            .ok_or(PgError::MissingVar(var))
50    }
51
52    /// Adds a new variable with the given initial value (and the inferred type) to the PG.
53    ///
54    /// ```
55    /// # use scan_core::program_graph::{PgExpression, ProgramGraphBuilder};
56    /// # let mut pg_builder = ProgramGraphBuilder::new();
57    /// // Create a new action
58    /// let action = pg_builder.new_action();
59    ///
60    /// // Create a value to assign the expression.
61    /// let val = (PgExpression::from(40i64) + PgExpression::from(40i64)).unwrap().eval_constant().unwrap();
62    ///
63    /// // Create a new variable
64    /// let var = pg_builder
65    ///     .new_var(val);
66    /// ```
67    pub fn new_var(&mut self, val: Val) -> Var {
68        let idx = self.vars.len();
69        self.vars.push(val);
70        Var(idx as u16)
71    }
72
73    /// Adds a new clock and returns a [`Clock`] id object.
74    ///
75    /// See also [`crate::channel_system::ChannelSystemBuilder::new_clock`].
76    pub fn new_clock(&mut self) -> Clock {
77        // We adopt the convention of indexing n clocks from 0 to n-1
78        let idx = self.clocks;
79        self.clocks += 1;
80        Clock(idx)
81    }
82
83    /// Adds a new action to the PG.
84    ///
85    /// ```
86    /// # use scan_core::program_graph::{Action, ProgramGraphBuilder};
87    /// # let mut pg_builder = ProgramGraphBuilder::new();
88    /// // Create a new action
89    /// let action: Action = pg_builder.new_action();
90    /// ```
91    #[inline(always)]
92    pub fn new_action(&mut self) -> Action {
93        let idx = self.effects.len();
94        self.effects.push(Effect::Effects(Vec::new(), Vec::new()));
95        Action(idx as ActionIdx)
96    }
97
98    /// Associates a clock reset to an action.
99    ///
100    /// Returns an error if the clock to be reset does not belong to the Program Graph.
101    ///
102    /// ```
103    /// # use scan_core::program_graph::{Clock, ProgramGraphBuilder};
104    /// # let mut pg_builder = ProgramGraphBuilder::new();
105    /// # let mut other_pg_builder = ProgramGraphBuilder::new();
106    /// let action = pg_builder.new_action();
107    /// let clock = other_pg_builder.new_clock();
108    /// // Associate action with clock reset
109    /// pg_builder
110    ///     .add_reset(action, clock)
111    ///     .expect_err("the clock does not belong to this PG");
112    /// ```
113    pub fn add_reset(&mut self, action: Action, clock: Clock) -> Result<(), PgError> {
114        if action == EPSILON {
115            return Err(PgError::NoEffects);
116        }
117        if clock.0 >= self.clocks {
118            return Err(PgError::MissingClock(clock));
119        }
120        match self
121            .effects
122            .get_mut(action.0 as usize)
123            .ok_or(PgError::MissingAction(action))?
124        {
125            Effect::Effects(_, resets) => {
126                resets.push(clock);
127                Ok(())
128            }
129            Effect::Send(_) => Err(PgError::EffectOnSend),
130            Effect::Receive(_) => Err(PgError::EffectOnReceive),
131        }
132    }
133
134    /// Adds an effect to the given action.
135    /// Requires specifying which variable is assigned the value of which expression whenever the action triggers a transition.
136    ///
137    /// It fails if the type of the variable and that of the expression do not match.
138    ///
139    /// ```
140    /// # use scan_core::program_graph::{Action, PgExpression, ProgramGraphBuilder, Var};
141    /// # use scan_core::Val;
142    /// # let mut pg_builder = ProgramGraphBuilder::new();
143    /// // Create a new action
144    /// let action: Action = pg_builder.new_action();
145    ///
146    /// // Create a new variable
147    /// let var: Var = pg_builder.new_var(Val::from(true)).expect("expression is well-typed");
148    ///
149    /// // Add an effect to the action
150    /// pg_builder
151    ///     .add_effect(action, var, PgExpression::from(1i64))
152    ///     .expect_err("var is of type bool but expression is of type integer");
153    /// pg_builder
154    ///     .add_effect(action, var, PgExpression::from(false))
155    ///     .expect("var and expression type match");
156    /// ```
157    pub fn add_effect(
158        &mut self,
159        action: Action,
160        var: Var,
161        effect: PgExpression,
162    ) -> Result<(), PgError> {
163        if action == EPSILON {
164            return Err(PgError::NoEffects);
165        }
166        effect
167            .context(&|var| self.vars.get(var.0 as usize).map(|val| Val::r#type(*val)))
168            .map_err(PgError::Type)?;
169        let var_type = self
170            .vars
171            .get(var.0 as usize)
172            .map(|val| Val::r#type(*val))
173            .ok_or_else(|| PgError::MissingVar(var.to_owned()))?;
174        if var_type == effect.r#type() {
175            match self
176                .effects
177                .get_mut(action.0 as usize)
178                .ok_or(PgError::MissingAction(action))?
179            {
180                Effect::Effects(effects, _) => {
181                    effects.push((var, effect));
182                    Ok(())
183                }
184                Effect::Send(_) => Err(PgError::EffectOnSend),
185                Effect::Receive(_) => Err(PgError::EffectOnReceive),
186            }
187        } else {
188            Err(PgError::TypeMismatch)
189        }
190    }
191
192    pub(crate) fn new_send(&mut self, mut msgs: Vec<PgExpression>) -> Result<Action, PgError> {
193        // Check message is well-typed
194        msgs.iter()
195            .try_for_each(|msg| {
196                msg.context(&|var| self.vars.get(var.0 as usize).map(|val| Val::r#type(*val)))
197            })
198            .map_err(PgError::Type)?;
199        // Actions are indexed progressively
200        let idx = self.effects.len();
201        msgs.shrink_to_fit();
202        self.effects.push(Effect::Send(msgs));
203        Ok(Action(idx as ActionIdx))
204    }
205
206    pub(crate) fn new_receive(&mut self, mut vars: Vec<Var>) -> Result<Action, PgError> {
207        if let Some(var) = vars.iter().find(|var| self.vars.len() as u16 <= var.0) {
208            Err(PgError::MissingVar(var.to_owned()))
209        } else {
210            // Actions are indexed progressively
211            let idx = self.effects.len();
212            vars.shrink_to_fit();
213            self.effects.push(Effect::Receive(vars));
214            Ok(Action(idx as ActionIdx))
215        }
216    }
217
218    /// Adds a new location to the PG and returns its [`Location`] indexing object.
219    #[inline(always)]
220    pub fn new_location(&mut self) -> Location {
221        self.new_timed_location(Vec::new())
222            .expect("new untimed location")
223    }
224
225    /// Adds a new location to the PG with the given time invariants,
226    /// and returns its [`Location`] indexing object.
227    pub fn new_timed_location(
228        &mut self,
229        mut invariants: Vec<(Clock, TimeRange)>,
230    ) -> Result<Location, PgError> {
231        if let Some((clock, _)) = invariants.iter().find(|(c, _)| c.0 >= self.clocks) {
232            Err(PgError::MissingClock(*clock))
233        } else {
234            // Locations are indexed progressively
235            let idx = self.locations.len();
236            invariants.sort_unstable_by_key(|(c, _)| *c);
237            invariants.shrink_to_fit();
238            self.locations.push((BTreeMap::new(), invariants));
239            Ok(Location(idx as LocationIdx))
240        }
241    }
242
243    /// Adds a new (synchronous) process to the PG starting from the given [`Location`].
244    pub fn new_process(&mut self, location: Location) -> Result<(), PgError> {
245        self.locations
246            .get(location.0 as usize)
247            .ok_or(PgError::MissingLocation(location))?
248            .1 // location's time invariants
249            .iter()
250            .all(|(_, range)| {
251                // All clocks start at time 0
252                range.contains(&0)
253            })
254            .then(|| self.initial_states.push(location))
255            .ok_or(PgError::Invariant)
256    }
257
258    /// Adds a new process starting at a new location to the PG and returns the [`Location`] indexing object.
259    #[inline(always)]
260    pub fn new_initial_location(&mut self) -> Location {
261        self.new_initial_timed_location(Vec::new())
262            .expect("new untimed location")
263    }
264
265    /// Adds a new process starting at a new location to the PG with the given time invariants,
266    /// and returns the [`Location`] indexing object.
267    pub fn new_initial_timed_location(
268        &mut self,
269        invariants: Vec<(Clock, TimeRange)>,
270    ) -> Result<Location, PgError> {
271        let location = self.new_timed_location(invariants)?;
272        self.new_process(location)?;
273        Ok(location)
274    }
275
276    /// Adds a transition to the PG.
277    /// Requires specifying:
278    ///
279    /// - state pre-transition,
280    /// - action triggering the transition,
281    /// - state post-transition, and
282    /// - (optionally) boolean expression guarding the transition.
283    ///
284    /// Fails if the provided guard is not a boolean expression.
285    ///
286    /// ```
287    /// # use scan_core::program_graph::ProgramGraphBuilder;
288    /// # use scan_core::BooleanExpr;
289    /// # let mut pg_builder = ProgramGraphBuilder::new();
290    /// // The builder is initialized with an initial location
291    /// let initial_loc = pg_builder.new_initial_location();
292    ///
293    /// // Create a new action
294    /// let action = pg_builder.new_action();
295    ///
296    /// // Add a transition
297    /// pg_builder
298    ///     .add_transition(initial_loc, action, initial_loc, None)
299    ///     .expect("this transition can be added");
300    /// pg_builder
301    ///     .add_transition(initial_loc, action, initial_loc, Some(BooleanExpr::from(false)))
302    ///     .expect("this one too");
303    /// ```
304    #[inline(always)]
305    pub fn add_transition(
306        &mut self,
307        pre: Location,
308        action: Action,
309        post: Location,
310        guard: Option<PgGuard>,
311    ) -> Result<(), PgError> {
312        self.add_timed_transition(pre, action, post, guard, Vec::new())
313    }
314
315    /// Adds a timed transition to the PG under timed constraints.
316    /// Requires specifying the same data as [`ProgramGraphBuilder::add_transition`],
317    /// plus a slice of time constraints.
318    ///
319    /// Fails if the provided guard is not a boolean expression.
320    ///
321    /// ```
322    /// # use scan_core::program_graph::ProgramGraphBuilder;
323    /// # use scan_core::BooleanExpr;
324    /// # let mut pg_builder = ProgramGraphBuilder::new();
325    /// // The builder is initialized with an initial location
326    /// let initial_loc = pg_builder.new_initial_location();
327    ///
328    /// // Create a new action
329    /// let action = pg_builder.new_action();
330    ///
331    /// // Add a new clock
332    /// let clock = pg_builder.new_clock();
333    ///
334    /// // Add a timed transition
335    /// pg_builder
336    ///     .add_timed_transition(initial_loc, action, initial_loc, None, vec![(clock, None, Some(1))])
337    ///     .expect("this transition can be added");
338    /// pg_builder
339    ///     .add_timed_transition(initial_loc, action, initial_loc, Some(BooleanExpr::from(false)), vec![(clock, Some(1), None)])
340    ///     .expect("this one too");
341    /// ```
342    pub fn add_timed_transition(
343        &mut self,
344        pre: Location,
345        action: Action,
346        post: Location,
347        guard: Option<PgGuard>,
348        mut constraints: Vec<(Clock, TimeRange)>,
349    ) -> Result<(), PgError> {
350        // Check 'pre' and 'post' locations exists
351        if self.locations.len() as LocationIdx <= pre.0 {
352            Err(PgError::MissingLocation(pre))
353        } else if self.locations.len() as LocationIdx <= post.0 {
354            Err(PgError::MissingLocation(post))
355        } else if action != EPSILON && self.effects.len() as ActionIdx <= action.0 {
356            // Check 'action' exists
357            Err(PgError::MissingAction(action))
358        } else if let Some((clock, _)) = constraints.iter().find(|(c, _)| c.0 >= self.clocks) {
359            Err(PgError::MissingClock(*clock))
360        } else {
361            if let Some(ref guard) = guard {
362                guard
363                    .context(&|var| self.vars.get(var.0 as usize).map(|val| Val::r#type(*val)))
364                    .map_err(PgError::Type)?;
365            }
366            let (transitions, _) = &mut self.locations[pre.0 as usize];
367            constraints.sort_unstable_by_key(|(c, _)| *c);
368            constraints.shrink_to_fit();
369            let transition = (post, guard, constraints);
370            // WARN: Actions have to be inserted in order but insertion has worst-case complexity O(n)
371            // so in some cases insertion of all actions could have complexity O(n^2).
372            // In practice though this is unlikely to ever be a bottleneck
373            match transitions.get_mut(&action) {
374                Some(action_transitions) => action_transitions.push(transition),
375                None => {
376                    let _ = transitions.insert(action, vec![transition]);
377                }
378            }
379            Ok(())
380        }
381    }
382
383    /// Adds an autonomous transition to the PG, i.e., a transition enabled by the epsilon action.
384    /// Requires specifying:
385    ///
386    /// - state pre-transition,
387    /// - state post-transition, and
388    /// - (optionally) boolean expression guarding the transition.
389    ///
390    /// Fails if the provided guard is not a boolean expression.
391    ///
392    /// ```
393    /// # use scan_core::program_graph::ProgramGraphBuilder;
394    /// # use scan_core::BooleanExpr;
395    /// # let mut pg_builder = ProgramGraphBuilder::new();
396    /// // The builder is initialized with an initial location
397    /// let initial_loc = pg_builder.new_initial_location();
398    ///
399    /// // Add a transition
400    /// pg_builder
401    ///     .add_autonomous_transition(initial_loc, initial_loc, None)
402    ///     .expect("this autonomous transition can be added");
403    /// pg_builder
404    ///     .add_autonomous_transition(initial_loc, initial_loc, Some(BooleanExpr::from(false)))
405    ///     .expect("this one too");
406    /// ```
407    #[inline(always)]
408    pub fn add_autonomous_transition(
409        &mut self,
410        pre: Location,
411        post: Location,
412        guard: Option<PgGuard>,
413    ) -> Result<(), PgError> {
414        self.add_transition(pre, EPSILON, post, guard)
415    }
416
417    /// Adds an autonomous timed transition to the PG, i.e., a transition enabled by the epsilon action under time constraints.
418    /// Requires specifying the same data as [`ProgramGraphBuilder::add_autonomous_transition`],
419    /// plus a slice of time constraints.
420    ///
421    /// Fails if the provided guard is not a boolean expression.
422    ///
423    /// ```
424    /// # use scan_core::program_graph::ProgramGraphBuilder;
425    /// # use scan_core::BooleanExpr;
426    /// # let mut pg_builder = ProgramGraphBuilder::new();
427    /// // The builder is initialized with an initial location
428    /// let initial_loc = pg_builder.new_initial_location();
429    ///
430    /// // Add a new clock
431    /// let clock = pg_builder.new_clock();
432    ///
433    /// // Add an autonomous timed transition
434    /// pg_builder
435    ///     .add_autonomous_timed_transition(initial_loc, initial_loc, None, vec![(clock, None, Some(1))])
436    ///     .expect("this transition can be added");
437    /// pg_builder
438    ///     .add_autonomous_timed_transition(initial_loc, initial_loc, Some(BooleanExpr::from(false)), vec![(clock, Some(1), None)])
439    ///     .expect("this one too");
440    /// ```
441    #[inline(always)]
442    pub fn add_autonomous_timed_transition(
443        &mut self,
444        pre: Location,
445        post: Location,
446        guard: Option<PgGuard>,
447        constraints: Vec<(Clock, TimeRange)>,
448    ) -> Result<(), PgError> {
449        self.add_timed_transition(pre, EPSILON, post, guard, constraints)
450    }
451
452    /// Produces a [`ProgramGraph`] defined by the [`ProgramGraphBuilder`]'s data and consuming it.
453    ///
454    /// Since the construction of the builder is already checked ad every step,
455    /// this method cannot fail.
456    pub fn build(mut self) -> ProgramGraph {
457        // Since vectors of effects and transitions will become immutable,
458        // they should be shrunk to take as little space as possible
459        self.initial_states.shrink_to_fit();
460        self.vars.shrink_to_fit();
461        self.effects.iter_mut().for_each(|effect| {
462            if let Effect::Effects(_, resets) = effect {
463                resets.sort_unstable();
464                resets.shrink_to_fit();
465            }
466        });
467        self.effects.shrink_to_fit();
468        let mut locations = self
469            .locations
470            .into_iter()
471            .map(|(transitions, loc_invariants)| {
472                let mut transitions = Vec::from_iter(transitions);
473                transitions.shrink_to_fit();
474                transitions.iter_mut().for_each(|(_, loc_transitions)| {
475                    assert!(!loc_transitions.is_empty());
476                    loc_transitions.sort_unstable_by_key(|(p, ..)| *p);
477                    loc_transitions.shrink_to_fit();
478                });
479                // Actions are assumed to be sorted
480                assert!(transitions.is_sorted_by_key(|(action, _)| *action));
481                (transitions, loc_invariants)
482            })
483            .collect::<Vec<_>>();
484        locations.shrink_to_fit();
485        // Build program graph
486        let pg = ProgramGraph {
487            initial_states: self.initial_states,
488            effects: self.effects,
489            locations,
490            vars: self.vars,
491            clocks: self.clocks,
492        };
493        info!(
494            "create Program Graph with: {} locations; {} actions; {} vars; size {}",
495            pg.locations.len(),
496            pg.effects.len(),
497            pg.vars.len(),
498            pg.get_size()
499        );
500        pg
501    }
502}