Skip to main content

expectation_graph/
flow.rs

1//! The flow DAG: how far each expectation is from the start, and what leads to it.
2//!
3//! Starting from the Background (and the state each story arranges), it searches breadth
4//! first: in each state, take every transition that can apply and see what state it leaves.
5//! An expectation's *level* is the fewest steps before it applies, and its parents are the
6//! expectations that take the last step on those shortest routes. Levels only grow along
7//! edges, so the result is a DAG even though the system's behaviour loops (start, run dry,
8//! reset, start again). Each expectation also gets a shortest *route*, which is a ready-made
9//! test for it.
10
11use crate::precedence::Precedence;
12use crate::semantics::{perform_among, settle};
13use expectation_model::{Condition, ExpId, Expectation, SectionId, Suite, VarId};
14use std::collections::{BTreeSet, HashMap};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub enum Node {
18    Start,
19    Exp(ExpId),
20}
21
22/// A way to reach an expectation: arrange `start`, then take each transition in `steps`.
23#[derive(Clone, Debug)]
24pub struct Route {
25    pub start: Condition,
26    pub steps: Vec<ExpId>,
27}
28
29#[derive(Clone, Copy, Debug)]
30pub struct Limits {
31    pub depth: u32,
32    pub states: usize,
33}
34
35impl Default for Limits {
36    fn default() -> Self {
37        Limits { depth: 24, states: 5000 }
38    }
39}
40
41#[derive(Debug, Default)]
42pub struct Flow {
43    /// Per expectation: the fewest steps from a start before it applies, if it ever does.
44    pub level: Vec<Option<u32>>,
45    /// (parent, child) edges of the DAG.
46    pub edges: BTreeSet<(Node, ExpId)>,
47    /// Per expectation: a shortest route to it.
48    pub routes: Vec<Option<Route>>,
49    pub states: usize,
50    /// The search hit its limits, so "unreachable" isn't conclusive.
51    pub truncated: bool,
52}
53
54impl Flow {
55    pub fn parents(&self, e: ExpId) -> impl Iterator<Item = Node> + '_ {
56        self.edges.iter().filter(move |(_, c)| *c == e).map(|(p, _)| *p)
57    }
58
59    pub fn children(&self, n: Node) -> impl Iterator<Item = ExpId> + '_ {
60        self.edges.iter().filter(move |(p, _)| *p == n).map(|(_, c)| *c)
61    }
62
63    fn record(&mut self, e: ExpId, level: u32, from: Node, route: impl FnOnce() -> Route) {
64        let i = e.0 as usize;
65        match self.level[i] {
66            None => {
67                self.level[i] = Some(level);
68                self.routes[i] = Some(route());
69                self.edges.insert((from, e));
70            }
71            Some(l) if l == level => {
72                self.edges.insert((from, e));
73            }
74            _ => {}
75        }
76    }
77}
78
79struct State {
80    cond: Condition,
81    /// The section whose root it grew from: only that part of the suite applies.
82    section: SectionId,
83    level: u32,
84    parent: Option<usize>,
85    via: Option<ExpId>,
86    /// What to arrange to get here: the root's state plus whatever the route assumed.
87    start: Condition,
88    /// States some step on the way has changed (assumptions about them can't be arranged up front).
89    touched: BTreeSet<VarId>,
90}
91
92impl State {
93    fn from(&self) -> Node {
94        self.via.map_or(Node::Start, Node::Exp)
95    }
96}
97
98fn steps_to(states: &[State], mut i: usize) -> Vec<ExpId> {
99    let mut out = vec![];
100    while let (Some(p), Some(v)) = (states[i].parent, states[i].via) {
101        out.push(v);
102        i = p;
103    }
104    out.reverse();
105    out
106}
107
108/// The expectations that apply in a section: everything under the same top-level section, so
109/// an app section can use the rest of the app's expectations but not the API's. In the root, everything.
110pub fn in_scope(suite: &Suite, section: SectionId) -> Vec<bool> {
111    let mut scope = vec![false; suite.expectations.len()];
112    let chain = suite.chain(section);
113    let top = chain.get(1).unwrap_or(&chain[0]).id;
114    for s in suite.subtree(top) {
115        for &f in &suite.section(s).features {
116            for &e in &suite.feature(f).expectations {
117                scope[e.0 as usize] = true;
118            }
119        }
120    }
121    scope
122}
123
124/// Search from each root state (each section's start, and each story's arranged start),
125/// taking only the expectations in scope for the root's section.
126pub fn explore(suite: &Suite, prec: &Precedence, roots: &[(Condition, SectionId)], limits: Limits) -> Flow {
127    let n = suite.expectations.len();
128    let mut flow = Flow { level: vec![None; n], routes: vec![None; n], ..Flow::default() };
129    let mut states: Vec<State> = vec![];
130    let mut seen: HashMap<Condition, usize> = HashMap::new();
131
132    let mut scopes: HashMap<SectionId, Vec<bool>> = HashMap::new();
133    for (root, section) in roots {
134        let scope = scopes.entry(*section).or_insert_with(|| in_scope(suite, *section));
135        let (cond, invariants) = settle(suite, prec, root, &[]);
136        if !cond.is_satisfiable() || seen.contains_key(&cond) {
137            continue;
138        }
139        for inv in invariants.into_iter().filter(|e| scope[e.0 as usize]) {
140            flow.record(inv, 0, Node::Start, || Route { start: root.clone(), steps: vec![] });
141        }
142        seen.insert(cond.clone(), states.len());
143        let section = *section;
144        states.push(State {
145            cond,
146            section,
147            level: 0,
148            parent: None,
149            via: None,
150            start: root.clone(),
151            touched: BTreeSet::new(),
152        });
153    }
154
155    let transitions: Vec<&Expectation> = suite.transitions().collect();
156    let companions = companions(&transitions);
157    let mut i = 0;
158    while i < states.len() {
159        let scope = &scopes[&states[i].section];
160        for (t, e) in transitions.iter().enumerate() {
161            let s = &states[i];
162            if !scope[e.id.0 as usize] || !s.cond.overlaps(&e.given) {
163                continue;
164            }
165            // Take the part of this state where `e` applies. Assumptions about states nothing
166            // has changed yet can be arranged at the start.
167            let pre = s.cond.and(&e.given);
168            let out = perform_among(suite, prec, &pre, &e.trigger, companions[t].iter().map(|&c| transitions[c]));
169            if !out.fired.contains(&e.id) {
170                continue;
171            }
172            let assumed = s.cond.unproven(&e.given).restrict(e.given.vars().filter(|v| !s.touched.contains(v)));
173            let start = s.start.and(&assumed);
174            let route = || {
175                let mut steps = steps_to(&states, i);
176                steps.push(e.id);
177                Route { start: start.clone(), steps }
178            };
179            let (level, from) = (s.level, s.from());
180            for &f in &out.fired {
181                flow.record(f, level, from, route);
182            }
183            if !out.after.is_satisfiable() {
184                continue;
185            }
186            for &inv in out.invariants.iter().filter(|e| scope[e.0 as usize]) {
187                flow.record(inv, level + 1, Node::Exp(e.id), route);
188            }
189            if seen.contains_key(&out.after) {
190                continue;
191            }
192            if level + 1 > limits.depth || states.len() >= limits.states {
193                flow.truncated = true;
194                continue;
195            }
196            let mut touched = states[i].touched.clone();
197            touched.extend(e.trigger.changes.vars());
198            for &f in &out.fired {
199                touched.extend(suite.exp(f).sets());
200            }
201            seen.insert(out.after.clone(), states.len());
202            let section = states[i].section;
203            states.push(State {
204                cond: out.after,
205                section,
206                level: level + 1,
207                parent: Some(i),
208                via: Some(e.id),
209                start,
210                touched,
211            });
212        }
213        i += 1;
214    }
215    flow.states = states.len();
216    flow
217}
218
219/// For each transition, the transitions its own trigger might also set off.
220fn companions(transitions: &[&Expectation]) -> Vec<Vec<usize>> {
221    transitions
222        .iter()
223        .map(|e| (0..transitions.len()).filter(|&c| e.trigger.may_fire(&transitions[c].trigger)).collect())
224        .collect()
225}
226
227/// A short chain of transitions from `from` to a state where `goal` certainly holds, if one
228/// is within `limits`. How a section's setup is reached from its parent's.
229///
230/// It only takes transitions that change something relevant: a state the goal needs changed,
231/// or one some relevant transition's Given or trigger needs. Among those it expands first the
232/// state with the fewest steps taken plus half the goal states still wrong, so it reaches deep
233/// setups (sign in, open a project, pick a tool, start drawing) without trying every other button
234/// on the way.
235pub fn route_to(suite: &Suite, prec: &Precedence, from: &Condition, goal: &Condition, limits: Limits) -> Option<Route> {
236    use std::cmp::Reverse;
237    use std::collections::BinaryHeap;
238
239    // Relevant: the states the goal needs changed, and whatever the transitions that change
240    // them need. A transition that only touches states that are already right can only undo them.
241    let (start, _) = settle(suite, prec, from, &[]);
242    let mut relevant: BTreeSet<VarId> =
243        goal.iter().filter(|(v, g)| !start.get(*v).is_some_and(|x| x.within(g))).map(|(v, _)| v).collect();
244    loop {
245        let before = relevant.len();
246        for e in suite.transitions() {
247            if e.sets().any(|v| relevant.contains(&v)) {
248                relevant.extend(e.given.vars().chain(e.trigger.changes.vars()));
249            }
250        }
251        if relevant.len() == before {
252            break;
253        }
254    }
255    let transitions: Vec<&Expectation> =
256        suite.transitions().filter(|e| e.sets().any(|v| relevant.contains(&v))).collect();
257    let companions = companions(&transitions);
258    let wrong = |c: &Condition| goal.iter().filter(|(v, g)| !c.get(*v).is_some_and(|x| x.within(g))).count() as u32;
259
260    // (state, parent, via, steps taken)
261    let mut states: Vec<(Condition, Option<usize>, Option<ExpId>, u32)> = vec![(start.clone(), None, None, 0)];
262    let mut seen: HashMap<Condition, usize> = HashMap::from([(start.clone(), 0)]);
263    let mut queue = BinaryHeap::from([(Reverse(wrong(&start)), Reverse(0usize))]);
264    // Ordered by 2 × steps + wrong states, i.e. steps + wrong / 2, in whole numbers.
265    while let Some((_, Reverse(i))) = queue.pop() {
266        if states[i].0.implies(goal) {
267            let mut steps = vec![];
268            let mut at = i;
269            while let (Some(p), Some(v)) = (states[at].1, states[at].2) {
270                steps.push(v);
271                at = p;
272            }
273            steps.reverse();
274            // The search isn't exact: drop any step the route works without.
275            let replay = |steps: &[ExpId]| {
276                let mut state = start.clone();
277                for &id in steps {
278                    let t = transitions.iter().position(|e| e.id == id)?;
279                    let e = transitions[t];
280                    if !state.implies(&e.given) {
281                        return None;
282                    }
283                    let candidates = companions[t].iter().map(|&c| transitions[c]);
284                    let out = perform_among(suite, prec, &state, &e.trigger, candidates);
285                    if !out.fired.contains(&e.id) || !out.uncertain.is_empty() || !out.after.is_satisfiable() {
286                        return None;
287                    }
288                    state = out.after;
289                }
290                Some(state)
291            };
292            let mut k = 0;
293            while k < steps.len() {
294                let mut fewer = steps.clone();
295                fewer.remove(k);
296                if replay(&fewer).is_some_and(|end| end.implies(goal)) {
297                    steps = fewer;
298                } else {
299                    k += 1;
300                }
301            }
302            // Where the order doesn't matter, keep the suite's: the way its authors would tell
303            // it (arm, then AUTO). Swap neighbours that are out of order if the route still works.
304            let mut swapped = true;
305            while swapped {
306                swapped = false;
307                for k in 1..steps.len() {
308                    if steps[k] < steps[k - 1] {
309                        let mut other = steps.clone();
310                        other.swap(k - 1, k);
311                        if replay(&other).is_some_and(|end| end.implies(goal)) {
312                            steps = other;
313                            swapped = true;
314                        }
315                    }
316                }
317            }
318            return Some(Route { start: from.clone(), steps });
319        }
320        let depth = states[i].3;
321        if depth >= limits.depth {
322            continue;
323        }
324        for (t, e) in transitions.iter().enumerate() {
325            // Only from states where it certainly applies: the route has to work as written.
326            if states.len() >= limits.states || !states[i].0.implies(&e.given) {
327                continue;
328            }
329            let out =
330                perform_among(suite, prec, &states[i].0, &e.trigger, companions[t].iter().map(|&c| transitions[c]));
331            if !out.fired.contains(&e.id) || !out.uncertain.is_empty() || !out.after.is_satisfiable() {
332                continue;
333            }
334            if !seen.contains_key(&out.after) {
335                let n = states.len();
336                seen.insert(out.after.clone(), n);
337                // Half the wrong goal states: most steps put right one or two.
338                queue.push((Reverse(2 * (depth + 1) + wrong(&out.after)), Reverse(n)));
339                states.push((out.after, Some(i), Some(e.id), depth + 1));
340            }
341        }
342    }
343    None
344}