Skip to main content

tla_eval/
diagnose.rs

1//! Why a step is not allowed.
2//!
3//! Rejecting a transition is easy; saying what the implementation was *trying*
4//! to do is the useful part. A `Next` is a disjunction of actions, and an
5//! action is a conjunction of a guard and an effect, so the interesting fact
6//! about a rejected step is which action came closest and which of its
7//! conjuncts stopped it.
8//!
9//! A model checker cannot report this, because it never evaluates the
10//! specification at the offending pair of states — it searches for the pair and
11//! fails to find it. Evaluating gets the answer for free.
12
13use std::collections::BTreeMap;
14
15use tla_syntax::token::Op;
16use tla_syntax::{Def, Expr, Param, QuantKind};
17
18use crate::error::Result;
19use crate::eval::{Ctx, Evaluator, Local, State, push};
20use crate::value::Value;
21
22/// How close one action came to permitting the step.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Blocked {
25    /// The action as it was reached, with arguments at the values they took:
26    /// `Timeout(s = "s1")`.
27    pub action: String,
28    /// How many of the action's conjuncts hold. Counted over all of them, not
29    /// just up to the first failure: an action wanting one guard it does not
30    /// have is much closer to firing than one whose every clause is wrong, and
31    /// stopping early cannot tell them apart.
32    pub satisfied: usize,
33    pub total: usize,
34    /// The first conjunct that does not hold.
35    pub conjunct: String,
36    /// Whether that conjunct constrains the successor state. False means the
37    /// action was not available; true means it was, but produces a different
38    /// next state than the one taken.
39    pub about_next_state: bool,
40    /// Set when the conjunct could not be evaluated rather than evaluating to
41    /// FALSE — a guard that would have protected it has already failed.
42    pub error: Option<String>,
43}
44
45impl Blocked {
46    /// Closeness as a fraction of the action's conjuncts, compared without
47    /// floating point.
48    fn closer_than(&self, other: &Self) -> std::cmp::Ordering {
49        (self.satisfied * other.total).cmp(&(other.satisfied * self.total))
50    }
51}
52
53/// Actions are not followed deeper than this; a specification that nests
54/// disjunctions further is reported on as far as it was explored.
55const MAX_DEPTH: usize = 16;
56
57#[derive(Default)]
58struct Probe {
59    /// The closest attempt at each named action. Keyed by name alone, so an
60    /// action quantified over several bindings is reported once, at its best.
61    best: BTreeMap<String, Blocked>,
62    /// Set when some action was found to permit the step after all.
63    allowed: bool,
64}
65
66impl<'m> Evaluator<'m> {
67    /// The actions that came closest to permitting `from -> to`, closest
68    /// first.
69    ///
70    /// Empty when the step is allowed. Every action other than the one taken
71    /// fails at such a step, and reporting those would be noise rather than a
72    /// diagnosis.
73    pub fn why_not(&self, name: &str, from: &State, to: &State) -> Result<Vec<Blocked>> {
74        let body = self.body_of(name)?;
75        let mut ctx = self.ctx(from, Some(to));
76        let mut found = Probe::default();
77        self.probe(body, &mut ctx, None, &mut found, 0)?;
78        if found.allowed {
79            return Ok(Vec::new());
80        }
81        let mut out: Vec<Blocked> = found.best.into_values().collect();
82        out.sort_by(|a, b| {
83            b.closer_than(a)
84                .then_with(|| b.satisfied.cmp(&a.satisfied))
85                .then_with(|| a.action.cmp(&b.action))
86        });
87        Ok(out)
88    }
89
90    /// Walk the disjunctive structure of an action, keeping track of which
91    /// named action the current branch belongs to.
92    fn probe(
93        &self,
94        e: &'m Expr,
95        ctx: &mut Ctx<'m, '_>,
96        label: Option<&str>,
97        found: &mut Probe,
98        depth: usize,
99    ) -> Result<()> {
100        if depth >= MAX_DEPTH {
101            return Ok(());
102        }
103        match e {
104            Expr::Binary(Op::Or, lhs, rhs) => {
105                self.probe(lhs, ctx, label, found, depth + 1)?;
106                self.probe(rhs, ctx, label, found, depth + 1)
107            }
108            Expr::Quant {
109                kind: QuantKind::Exists,
110                bounds,
111                body,
112            } => {
113                for binding in self.expand(bounds, ctx)? {
114                    let restore = push(ctx, &binding);
115                    let walked = self.probe(body, ctx, label, found, depth + 1);
116                    ctx.locals.truncate(restore);
117                    walked?;
118                }
119                Ok(())
120            }
121            Expr::Let { defs, body, .. } => {
122                let base = ctx.locals.len();
123                let scope = base + defs.len();
124                for def in defs {
125                    ctx.locals
126                        .push((def.name.clone(), Local::Def { def, scope }));
127                }
128                let walked = self.probe(body, ctx, label, found, depth + 1);
129                ctx.locals.truncate(base);
130                walked
131            }
132            Expr::Apply(head, args) => {
133                if let Expr::Ident(name) = &**head
134                    && let Some((_, def)) = self.spec.definition(ctx.module, name)
135                {
136                    return self.enter(def, args, ctx, found, depth);
137                }
138                self.record(e, ctx, label, found);
139                Ok(())
140            }
141            Expr::Ident(name) => {
142                if let Some((_, def)) = self.spec.definition(ctx.module, name)
143                    && def.params.is_empty()
144                {
145                    return self.enter(def, &[], ctx, found, depth);
146                }
147                self.record(e, ctx, label, found);
148                Ok(())
149            }
150            _ => {
151                self.record(e, ctx, label, found);
152                Ok(())
153            }
154        }
155    }
156
157    /// Step into a named action, naming it by the arguments it was given.
158    fn enter(
159        &self,
160        def: &'m Def,
161        args: &'m [Expr],
162        ctx: &mut Ctx<'m, '_>,
163        found: &mut Probe,
164        depth: usize,
165    ) -> Result<()> {
166        if def.params.len() != args.len() {
167            return Ok(());
168        }
169        let mut values = Vec::with_capacity(args.len());
170        for arg in args {
171            values.push(self.eval(arg, ctx)?);
172        }
173        let label = render_call(&def.name, &def.params, &values);
174
175        // An operator sees its parameters and the module, never the locals of
176        // whoever called it -- the same rule evaluation follows.
177        let hidden = std::mem::take(&mut ctx.locals);
178        for (param, value) in def.params.iter().zip(values) {
179            ctx.locals.push((param.name.clone(), Local::Val(value)));
180        }
181        let walked = self.probe(&def.body, ctx, Some(&label), found, depth + 1);
182        ctx.locals = hidden;
183        walked
184    }
185
186    /// Evaluate a leaf action conjunct by conjunct and keep the furthest it got.
187    fn record(&self, e: &'m Expr, ctx: &mut Ctx<'m, '_>, label: Option<&str>, found: &mut Probe) {
188        let parts = conjuncts(e);
189        let mut satisfied = 0;
190        let mut first_failure = None;
191        for part in &parts {
192            // A conjunct after a failed guard may not be evaluable at all --
193            // `Head(buf)` once `Len(buf) > 0` is false. That is a failure to
194            // satisfy it, not a reason to abandon the whole diagnosis.
195            match self.eval_bool(part, ctx) {
196                Ok(true) => satisfied += 1,
197                Ok(false) => {
198                    first_failure.get_or_insert((*part, None));
199                }
200                Err(e) => {
201                    first_failure.get_or_insert((*part, Some(e.to_string())));
202                }
203            }
204        }
205        let Some((part, error)) = first_failure else {
206            found.allowed = true;
207            return;
208        };
209
210        let action = label.map_or_else(|| truncate(&e.to_string()), ToString::to_string);
211        let candidate = Blocked {
212            action: action.clone(),
213            satisfied,
214            total: parts.len(),
215            conjunct: truncate(&part.to_string()),
216            about_next_state: part.mentions_next_state(),
217            error,
218        };
219        let key = action.split('(').next().unwrap_or(&action).to_string();
220        match found.best.get(&key) {
221            Some(existing) if existing.closer_than(&candidate).is_ge() => {}
222            _ => {
223                found.best.insert(key, candidate);
224            }
225        }
226    }
227}
228
229fn conjuncts(e: &Expr) -> Vec<&Expr> {
230    match e {
231        Expr::Binary(Op::And, lhs, rhs) => {
232            let mut out = conjuncts(lhs);
233            out.extend(conjuncts(rhs));
234            out
235        }
236        other => vec![other],
237    }
238}
239
240fn render_call(name: &str, params: &[Param], values: &[Value]) -> String {
241    if params.is_empty() {
242        return name.to_string();
243    }
244    let bindings: Vec<String> = params
245        .iter()
246        .zip(values)
247        .map(|(param, value)| format!("{} = {value}", param.name))
248        .collect();
249    format!("{name}({})", bindings.join(", "))
250}
251
252const MAX_RENDERED: usize = 160;
253
254fn truncate(text: &str) -> String {
255    if text.chars().count() <= MAX_RENDERED {
256        return text.to_string();
257    }
258    let head: String = text.chars().take(MAX_RENDERED).collect();
259    format!("{head}...")
260}