dialectic_compiler/
flow.rs

1//! Control flow analysis for the session type CFG.
2
3use {
4    std::collections::{HashSet, VecDeque},
5    thunderdome::Index,
6};
7
8use crate::cfg::{Cfg, Ir};
9
10/// The output of a flow analysis is two sets: the "passable" set, indicating which nodes have a
11/// possible path "through" them to their continuation - and the "haltable" set, indicating which
12/// nodes can possibly be run all the way to `Done`.
13#[derive(Debug, Clone)]
14pub struct FlowAnalysis {
15    passable: HashSet<Index>,
16    haltable: HashSet<Index>,
17}
18
19impl FlowAnalysis {
20    /// Shorthand for checking whether a node is passable.
21    pub fn is_passable(&self, node: Index) -> bool {
22        self.passable.contains(&node)
23    }
24}
25
26/// Analyze a CFG for passability of every contained node.
27pub fn analyze(cfg: &Cfg) -> FlowAnalysis {
28    Solver::new(cfg).solve()
29}
30
31/// Flow analysis works by using a constraint solver: we try to solve for whether every node in the
32/// graph is "passable", and in the process of doing so, we also solve "haltable" and "breaks from
33/// within some node or its children to some given parent loop node" constraints.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub enum Constraint {
36    /// A `Passable` constraint carries the information of which node we are trying to check is
37    /// passable or not.
38    Passable(Index),
39    /// A `Haltable` constraint for checking whether or not a node is "haltable".
40    Haltable(Index),
41    /// A `BreakableTo` constraint for checking whether or not a loop can be "broken to" by a node
42    /// within its body.
43    BreakableTo {
44        /// The node we want to check for a `Break`.
45        breaks_from: Index,
46        /// The loop we want to `Break` to.
47        breaks_to: Index,
48    },
49}
50
51/// A conjunction of constraints: true if *all* are true.
52pub type Conjunction = Vec<Constraint>;
53
54/// A disjunction of conjunctions of constraints: true if *any* of the clauses contain constraints
55/// *all of which* are true.
56#[derive(Debug, Clone)]
57pub struct Dnf(pub Vec<Conjunction>);
58
59impl Dnf {
60    /// The trivially false disjunction, unprovable no matter what.
61    pub fn trivially_false() -> Self {
62        Dnf(vec![])
63    }
64
65    /// The trivially true disjunction, provable with no evidence.
66    pub fn trivially_true() -> Self {
67        Dnf(vec![vec![]])
68    }
69
70    /// Tests if a conjunction is *trivially* true (i.e. requires no evidence to be true).
71    pub fn is_trivially_true(&self) -> bool {
72        self.0.iter().any(|c| c.is_empty())
73    }
74
75    /// Construct a disjunction from a single conjunction.
76    pub fn only_if(conj: Conjunction) -> Self {
77        Dnf(vec![conj])
78    }
79}
80
81/// An implication in normal form: a set of preconditions which prove a consequence.
82///
83/// When we can satisfy the preconditions (listed in disjunctive normal form), we are allowed to
84/// learn that the consequence is true and insert it into our set of knowledge, which we use to
85/// prove further preconditions to be true.
86#[derive(Debug)]
87pub struct Implication {
88    /// The preconditions to the implication.
89    pub preconditions: Dnf,
90    /// The consequent of the implication.
91    pub consequent: Constraint,
92}
93
94/// A result from breaking apart a DNF and attempting to determine whether it holds.
95#[derive(Debug, Clone, Copy)]
96pub enum Validity {
97    /// The DNF was proven to be true immediately, based on the knowledge available.
98    Trivial,
99    /// Progress was made and the solver state was changed by adding new things to be proven.
100    Progress,
101    /// No progress was made because all the component [`Constraint`]s of the DNF were already in
102    /// process of proof, or had been proven.
103    NoProgress,
104}
105
106/// The current state of the flow analysis solver.
107#[derive(Debug)]
108pub struct Solver<'a> {
109    /// The [`Cfg`] being solved for in this invocation of the analysis.
110    cfg: &'a Cfg,
111    /// The set of nodes which are currently proven to satisfy the `Passable` constraint.
112    passable: HashSet<Index>,
113    /// The set of nodes which are currently proven to satisfy the `Haltable` constraint.
114    haltable: HashSet<Index>,
115    /// The set of pairs of nodes which are currently proven to satisfy the `BreakableTo`
116    /// constraint, in the order of `breaks_from` followed by `breaks_to`.
117    breakable_to: HashSet<(Index, Index)>,
118    /// The number of implications examined since the last progress was made. (If this exceeds the
119    /// length of the implication queue, the solver cannot make any more progress.)
120    progress: usize,
121    /// The queue of [`Implication`]s yet to be proven.
122    queue: VecDeque<Implication>,
123    /// The set of all [`Constraint`]s which either have been proven or are in-progress.
124    queried: HashSet<Constraint>,
125}
126
127impl<'a> Solver<'a> {
128    /// Create a new [`Solver`] ready to calculate the flow analysis for the given [`Cfg`].
129    pub fn new(cfg: &'a Cfg) -> Self {
130        let mut this = Self {
131            cfg,
132            passable: HashSet::new(),
133            haltable: HashSet::new(),
134            breakable_to: HashSet::new(),
135            progress: 0,
136            queue: VecDeque::new(),
137            queried: HashSet::new(),
138        };
139
140        // Populate initial set of constraints to solve
141        for (index, _) in cfg.iter() {
142            this.push(Constraint::Passable(index));
143        }
144
145        this
146    }
147
148    /// Insert a fact into the solver's set of proven knowledge. This should only be called when
149    /// this fact has been shown to be true!
150    fn insert_fact(&mut self, constraint: Constraint) {
151        match constraint {
152            Constraint::Passable(node) => self.passable.insert(node),
153            Constraint::Haltable(node) => self.haltable.insert(node),
154            Constraint::BreakableTo {
155                breaks_from,
156                breaks_to,
157                ..
158            } => self.breakable_to.insert((breaks_from, breaks_to)),
159        };
160    }
161
162    /// Push a constraint into the solver's state, returning whether or not it is currently known to
163    /// be true.
164    fn push(&mut self, constraint: Constraint) -> bool {
165        if self.queried.insert(constraint) {
166            let dnf = preconditions(self.cfg, constraint);
167
168            if dnf.is_trivially_true() {
169                // Optimization: if the precondition of an implication is trivially true, we don't
170                // need to examine its conditions, we can just assert the truth of its consequent
171                self.insert_fact(constraint);
172            } else {
173                self.queue.push_back(Implication {
174                    preconditions: dnf,
175                    consequent: constraint,
176                });
177            }
178
179            true
180        } else {
181            false
182        }
183    }
184
185    /// Pop a new yet-to-be-proven [`Implication`] from the [`Solver`]'s queue. If no more progress
186    /// can be made, or there are no more implications left, returns [`None`].
187    fn pop(&mut self) -> Option<Implication> {
188        if self.progress > self.queue.len() {
189            None
190        } else {
191            self.progress += 1;
192            self.queue.pop_front()
193        }
194    }
195
196    /// Test if a constraint currently holds, given what we already know.
197    fn constraint_holds(&self, constraint: &Constraint) -> bool {
198        match constraint {
199            Constraint::Passable(node) => self.passable.contains(node),
200            Constraint::Haltable(node) => self.haltable.contains(node),
201            Constraint::BreakableTo {
202                breaks_from,
203                breaks_to,
204            } => self.breakable_to.contains(&(*breaks_from, *breaks_to)),
205        }
206    }
207
208    /// Attempt to prove that a DNF holds. If it can not yet be shown to be true, insert its atomic
209    /// constraints so they can be proven in the future, potentially.
210    fn try_prove(&mut self, dnf: &Dnf) -> Validity {
211        if dnf
212            .0
213            .iter()
214            .any(|cj| cj.iter().all(|c| self.constraint_holds(c)))
215        {
216            Validity::Trivial
217        } else {
218            let mut progress = false;
219            for &constraint in dnf.0.iter().flatten() {
220                progress |= self.push(constraint);
221            }
222
223            if progress {
224                Validity::Progress
225            } else {
226                Validity::NoProgress
227            }
228        }
229    }
230
231    /// Run the full flow analysis algorithm and return a solution.
232    pub fn solve(mut self) -> FlowAnalysis {
233        while let Some(implication) = self.pop() {
234            let validity = self.try_prove(&implication.preconditions);
235
236            if let Validity::Trivial = validity {
237                self.insert_fact(implication.consequent);
238            } else {
239                self.queue.push_back(implication);
240            }
241
242            if let Validity::Trivial | Validity::Progress = validity {
243                self.progress = 0;
244            }
245        }
246
247        FlowAnalysis {
248            passable: self.passable,
249            haltable: self.haltable,
250        }
251    }
252}
253
254/// Given a [`Constraint`] and the [`Cfg`] to which it pertains, compute the preconditions which
255/// would be necessary to satisfy that constraint.
256fn preconditions(cfg: &Cfg, constraint: Constraint) -> Dnf {
257    return match constraint {
258        Constraint::Passable(node) => passable_preconditions(cfg, node),
259        Constraint::Haltable(node) => haltable_preconditions(cfg, node),
260        Constraint::BreakableTo {
261            breaks_from,
262            breaks_to,
263        } => breakable_to_preconditions(cfg, breaks_from, breaks_to),
264    };
265
266    /// Compute the preconditions for a `Passable` constraint.
267    fn passable_preconditions(cfg: &Cfg, node: Index) -> Dnf {
268        match &cfg[node].expr {
269            // Trivially, `Send`, `Recv`, and `Call(None)` nodes are passable. We also assume that
270            // for any `Type` node, the external type it references is passable, and that for any
271            // `Error` node, assuming it passable will give the most possible relevant errors
272            // reported.
273            Ir::Send(_) | Ir::Recv(_) | Ir::Type(_) | Ir::Error | Ir::Call(None) => {
274                Dnf::trivially_true()
275            }
276            // Trivially, `Break` and `Continue` nodes are never passable, because they always jump
277            // and never call their continuations.
278            Ir::Break(_) | Ir::Continue(_) => Dnf::trivially_false(),
279            // `Call` nodes are only passable if their continuations are haltable.
280            Ir::Call(Some(callee)) => Dnf::only_if(vec![Constraint::Haltable(*callee)]),
281            // Similarly to call, split nodes are only passable if both arms are haltable.
282            Ir::Split { tx_only, rx_only } => {
283                let arms = tx_only.iter().chain(rx_only);
284                Dnf::only_if(arms.map(|&arm| Constraint::Haltable(arm)).collect())
285            }
286            // `Offer` and `Choose` nodes are passable if any arm is passable; however, the
287            // passability of an `Offer` or `Choose` typically does not matter because as control
288            // flow analysis must be run after scope resolution, they should not have an implicit
289            // continuation.
290            Ir::Offer(choices) | Ir::Choose(choices) => Dnf(choices
291                .iter()
292                .filter_map(Option::as_ref)
293                .map(|&c| vec![Constraint::Passable(c)])
294                .collect()),
295            // The `Loop` case is the most complex case here. A `Loop` only has its continuation
296            // called when a `Break` occurs targeting the loop in question, so it is only passable
297            // if it is `BreakableTo` from some node inside it.
298            Ir::Loop(body) => Dnf::only_if(vec![Constraint::BreakableTo {
299                breaks_from: body
300                    .expect("loop bodies should always be `Some` after scope resolution"),
301                breaks_to: node,
302            }]),
303        }
304    }
305
306    /// Compute the preconditions for a `Haltable` constraint.
307    fn haltable_preconditions(cfg: &Cfg, node_index: Index) -> Dnf {
308        let node = &cfg[node_index];
309        match &node.expr {
310            // Trivially, `Send`, `Recv`, and `Type` are only haltable if they continue to a
311            // haltable node (as they always continue.) We also assume `Error` behaves this way to
312            // try to get the maximum possible number of reported relevant errors.
313            Ir::Send(_) | Ir::Recv(_) | Ir::Type(_) | Ir::Error => match node.next {
314                Some(cont) => Dnf::only_if(vec![Constraint::Haltable(cont)]),
315                None => Dnf::trivially_true(),
316            },
317            // If a node is passable and its continuation halts, then it is always haltable. Instead
318            // of spitting out constraints on the child nodes of these cases, we rely on their
319            // definition of passability. Note that if the continuation is `None`, then the node
320            // halts only if it is passable, so in that case a `Haltable` constraint on it is not
321            // generated.
322            Ir::Call(_) | Ir::Split { .. } | Ir::Loop(_) => {
323                let mut conj = vec![Constraint::Passable(node_index)];
324                conj.extend(node.next.map(Constraint::Haltable));
325                Dnf::only_if(conj)
326            }
327            // `Choose`/`Offer` are haltable only if any of their choices are haltable.
328            Ir::Choose(choices) | Ir::Offer(choices) => Dnf(choices
329                .iter()
330                // If any of the choices are `Done`, we want to emit an empty `Vec` instead,
331                // denoting that this constraint is trivially satisfiable.
332                .map(|&c| c.map(Constraint::Haltable).into_iter().collect())
333                .chain(node.next.map(|c| vec![Constraint::Haltable(c)]))
334                .collect()),
335            // A `Continue` only halts if whatever it jumps to halts.
336            Ir::Continue(of_loop) => Dnf::only_if(vec![Constraint::Haltable(*of_loop)]),
337            // Similarly, a `Break` is a kind of jump to the continuation of its relevant loop, and
338            // halts only if the continuation of the loop halts.
339            Ir::Break(of_loop) => match cfg[*of_loop].next {
340                Some(cont) => Dnf::only_if(vec![Constraint::Haltable(cont)]),
341                None => Dnf::trivially_true(),
342            },
343        }
344    }
345
346    /// Compute the preconditions for a `BreakableTo` constraint.
347    fn breakable_to_preconditions(cfg: &Cfg, breaks_from: Index, breaks_to: Index) -> Dnf {
348        let node = &cfg[breaks_from];
349        // For most nodes, `BreakableTo` is inductive on its continuation.
350        match &node.expr {
351            // Trivially, `Send`, `Recv`, and `Type` will only break to a loop if their continuation
352            // does... as obviously they do not break. Though its behavior is undefined, we assume
353            // `Error` also behaves this way.
354            Ir::Send(_) | Ir::Recv(_) | Ir::Type(_) | Ir::Error => match node.next {
355                Some(cont) => Dnf::only_if(vec![Constraint::BreakableTo {
356                    breaks_from: cont,
357                    breaks_to,
358                }]),
359                None => Dnf::trivially_false(),
360            },
361            // `Call` and `Split` may break from within, but that doesn't actually count as them
362            // being breakable to a loop. Instead, similarly to `Send`, `Recv`, and `Type`, they are
363            // `BreakableTo` only if their continuation is, but we must also know whether they are
364            // passable (so that we can know if control flow can reach that break if it is present.)
365            Ir::Call(_) | Ir::Split { .. } => {
366                let mut conj = vec![Constraint::Passable(breaks_from)];
367                conj.extend(node.next.map(|cont| Constraint::BreakableTo {
368                    breaks_from: cont,
369                    breaks_to,
370                }));
371                Dnf::only_if(conj)
372            }
373            // `Choose` and `Offer` break only if any arm breaks.
374            Ir::Choose(choices) | Ir::Offer(choices) => Dnf(choices
375                .iter()
376                .filter_map(Option::as_ref)
377                .chain(node.next.as_ref())
378                .map(|&c| {
379                    vec![Constraint::BreakableTo {
380                        breaks_from: c,
381                        breaks_to,
382                    }]
383                })
384                .collect()),
385            // A loop only breaks to another loop in one of two cases: 1.) it contains a break
386            // targeting the loop in question. 2.) it is passable, and once it is passed, its
387            // continuation may break to the target.
388            Ir::Loop(body) => {
389                // Option 1.: the loop's body is `BreakableTo` the loop in question.
390                let mut disj = vec![vec![Constraint::BreakableTo {
391                    breaks_from: body
392                        .expect("loop bodies should be nonempty after scope resolution"),
393                    breaks_to,
394                }]];
395
396                // If there is a continuation, then option 2.: the loop's continuation is
397                // `BreakableTo` the loop in question.
398                if let Some(cont) = node.next {
399                    disj.push(vec![
400                        Constraint::Passable(breaks_from),
401                        Constraint::BreakableTo {
402                            breaks_from: cont,
403                            breaks_to,
404                        },
405                    ]);
406                }
407
408                Dnf(disj)
409            }
410            // If we find a break here to the target loop, we've solved this constraint.
411            Ir::Break(of_loop) if *of_loop == breaks_to => Dnf::trivially_true(),
412            // Any other breaks can be ignored. `Continue` nodes may also be ignored, as they simply
413            // repeat nodes we've already seen.
414            Ir::Break(_) | Ir::Continue(_) => Dnf::trivially_false(),
415        }
416    }
417}