1use 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#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Blocked {
25 pub action: String,
28 pub satisfied: usize,
33 pub total: usize,
34 pub conjunct: String,
36 pub about_next_state: bool,
40 pub error: Option<String>,
43}
44
45impl Blocked {
46 fn closer_than(&self, other: &Self) -> std::cmp::Ordering {
49 (self.satisfied * other.total).cmp(&(other.satisfied * self.total))
50 }
51}
52
53const MAX_DEPTH: usize = 16;
56
57#[derive(Default)]
58struct Probe {
59 best: BTreeMap<String, Blocked>,
62 allowed: bool,
64}
65
66impl<'m> Evaluator<'m> {
67 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 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 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 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 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 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}