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}