quint_connect/driver/
step.rs

1use crate::{
2    driver::{Config, Path, nondet::NondetPicks},
3    value::ValueDisplay,
4};
5use anyhow::{Context, Result, anyhow, bail};
6use itf::value::{Record, Value};
7use serde::Deserialize;
8use std::fmt;
9
10/// Represents a single step in a trace generated from a Quint specification.
11///
12/// Steps are passed to [`Driver::step`](crate::Driver::step) for execution against
13/// the implementation. Use the [`switch!`](crate::switch) macro to pattern-match
14/// on action names and extract nondeterministic picks from steps.
15pub struct Step {
16    #[doc(hidden)] // public for macro use
17    pub action_taken: String,
18    #[doc(hidden)] // public for macro use
19    pub nondet_picks: NondetPicks,
20    pub(crate) state: Value,
21}
22
23impl Step {
24    pub(crate) fn new(state: Record, config: &Config) -> Result<Self> {
25        if config.nondet.is_empty() {
26            extract_from_mbt_vars(state, config.state)
27        } else {
28            extract_from_sum_type(state, config.nondet, config.state)
29        }
30    }
31}
32
33fn extract_from_mbt_vars(mut state: Record, state_path: Path) -> Result<Step> {
34    Ok(Step {
35        action_taken: extract_action_from_mbt_var(&mut state)?,
36        nondet_picks: extract_nondet_from_mbt_var(&mut state)?,
37        state: extract_value_in_path(state, state_path)?,
38    })
39}
40
41fn extract_from_sum_type(mut state: Record, sum_type_path: Path, state_path: Path) -> Result<Step> {
42    let sum_type = find_record_in_path(&state, sum_type_path)?;
43    let action_taken = extract_action_from_sum_type(sum_type)?;
44    let nondet_picks = extract_nondet_from_sum_type(sum_type)?;
45
46    // Remove unused mbt variables, if available.
47    let _ = state.remove("mbt::actionTaken");
48    let _ = state.remove("mbt::nondetPicks");
49
50    let state = extract_value_in_path(state, state_path)?;
51
52    Ok(Step {
53        action_taken,
54        nondet_picks,
55        state,
56    })
57}
58
59fn extract_action_from_mbt_var(state: &mut Record) -> Result<String> {
60    state
61        .remove("mbt::actionTaken")
62        .ok_or(anyhow!("Missing `mbt::actionTaken` variable in the trace"))
63        .and_then(|value| {
64            String::deserialize(value).context("Failed to decode `mbt::actionTaken` variable")
65        })
66}
67
68fn extract_nondet_from_mbt_var(state: &mut Record) -> Result<NondetPicks> {
69    state
70        .remove("mbt::nondetPicks")
71        .ok_or(anyhow!("Missing `mbt::nondetPicks` variable in the trace"))
72        .and_then(|value| {
73            NondetPicks::new(value).context("Failed to extract nondet picks from trace")
74        })
75}
76
77fn extract_value_in_path(state: Record, path: &[&str]) -> Result<Value> {
78    let mut value = Value::Record(state);
79    for segment in path {
80        let Value::Record(mut rec) = value else {
81            bail!(
82                "Can not read {:?} from non-record value in path: {:?}\n\
83                 Current value: {}",
84                segment,
85                path,
86                value.display(),
87            )
88        };
89        let Some(next) = rec.remove(segment) else {
90            bail!(
91                "Can not find a value at {:?} in path: {:?}\n\
92                 Current value: {}",
93                segment,
94                path,
95                Value::Record(rec).display()
96            )
97        };
98        value = next
99    }
100    Ok(value)
101}
102
103fn find_record_in_path<'a>(state: &'a Record, path: &[&str]) -> Result<&'a Record> {
104    let mut rec = state;
105    for segment in path {
106        let Some(Value::Record(next)) = rec.get(segment) else {
107            bail!(
108                "Can not find a Record at {:?} in path: {:?}\n\
109                 Current state: {}",
110                segment,
111                path,
112                state.display()
113            )
114        };
115        rec = next;
116    }
117    Ok(rec)
118}
119
120fn extract_action_from_sum_type(ty: &Record) -> Result<String> {
121    let Some(Value::String(action)) = ty.get("tag") else {
122        bail!(
123            "Expected action to be a sum type variant.\n\
124             Value found: {}",
125            ty.display()
126        )
127    };
128    Ok(action.clone())
129}
130
131fn extract_nondet_from_sum_type(ty: &Record) -> Result<NondetPicks> {
132    match ty.get("value") {
133        Some(Value::Tuple(t)) if t.is_empty() => Ok(NondetPicks::empty()),
134        Some(Value::Record(rec)) => Ok(rec.clone().into()),
135        _ => bail!(
136            "Expected nondet picks to be a sum type variant value as a record.\n\
137             Value found: {}",
138            ty.display()
139        ),
140    }
141}
142
143impl fmt::Display for Step {
144    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
145        write!(f, "Action taken:")?;
146        if self.action_taken.is_empty() {
147            writeln!(f, " <anonymous>")?;
148        } else {
149            writeln!(f, " {}", self.action_taken)?;
150        }
151
152        write!(f, "Nondet picks:")?;
153        if self.nondet_picks.is_empty() {
154            writeln!(f, " <none>")?;
155        } else {
156            writeln!(f, "\n{}", self.nondet_picks)?;
157        }
158
159        write!(f, "Next state:")?;
160        match &self.state {
161            Value::Record(rec) => {
162                if rec.is_empty() {
163                    write!(f, " <none>")?;
164                } else {
165                    for (key, value) in rec.iter() {
166                        write!(f, "\n+ {}: {}", key, value.display())?;
167                    }
168                }
169            }
170            Value::Map(map) => {
171                if map.is_empty() {
172                    write!(f, " <none>")?;
173                } else {
174                    for (key, value) in map.iter() {
175                        write!(f, "\n+ {}: {}", key.display(), value.display())?;
176                    }
177                }
178            }
179            other => write!(f, " {}", other.display())?,
180        }
181        Ok(())
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use itf::Value;
189
190    #[test]
191    fn test_extract_value_in_path_empty_path() {
192        let mut rec = Record::new();
193        rec.insert("key".to_string(), Value::String("value".to_string()));
194
195        let result = extract_value_in_path(rec.clone(), &[]).unwrap();
196        assert_eq!(result, Value::Record(rec));
197    }
198
199    #[test]
200    fn test_extract_value_in_path_single_level() {
201        let mut rec = Record::new();
202        rec.insert("key".to_string(), Value::String("value".to_string()));
203
204        let result = extract_value_in_path(rec, &["key"]).unwrap();
205        assert_eq!(result, Value::String("value".to_string()));
206    }
207
208    #[test]
209    fn test_extract_value_in_path_nested() {
210        let mut inner = Record::new();
211        inner.insert("inner_key".to_string(), Value::Number(42));
212
213        let mut outer = Record::new();
214        outer.insert("outer_key".to_string(), Value::Record(inner));
215
216        let result = extract_value_in_path(outer, &["outer_key", "inner_key"]).unwrap();
217        assert_eq!(result, Value::Number(42));
218    }
219
220    #[test]
221    #[should_panic(expected = "Can not find a value at")]
222    fn test_extract_value_in_path_missing_key() {
223        let mut rec = Record::new();
224        rec.insert("key".to_string(), Value::String("value".to_string()));
225
226        extract_value_in_path(rec, &["missing"]).unwrap();
227    }
228
229    #[test]
230    #[should_panic(expected = "non-record value in path")]
231    fn test_extract_value_in_path_non_record() {
232        let mut rec = Record::new();
233        rec.insert("key".to_string(), Value::String("value".to_string()));
234
235        extract_value_in_path(rec, &["key", "nested"]).unwrap();
236    }
237
238    #[test]
239    fn test_find_record_in_path_empty_path() {
240        let rec = Record::new();
241        let result = find_record_in_path(&rec, &[]).unwrap();
242        assert_eq!(result, &rec);
243    }
244
245    #[test]
246    fn test_find_record_in_path_single_level() {
247        let inner = Record::new();
248        let mut outer = Record::new();
249        outer.insert("inner".to_string(), Value::Record(inner.clone()));
250
251        let result = find_record_in_path(&outer, &["inner"]).unwrap();
252        assert_eq!(result, &inner);
253    }
254
255    #[test]
256    fn test_find_record_in_path_nested() {
257        let innermost = Record::new();
258        let mut middle = Record::new();
259        middle.insert("innermost".to_string(), Value::Record(innermost.clone()));
260
261        let mut outer = Record::new();
262        outer.insert("middle".to_string(), Value::Record(middle));
263
264        let result = find_record_in_path(&outer, &["middle", "innermost"]).unwrap();
265        assert_eq!(result, &innermost);
266    }
267
268    #[test]
269    #[should_panic(expected = "Can not find a Record")]
270    fn test_find_record_in_path_missing_key() {
271        let rec = Record::new();
272        find_record_in_path(&rec, &["missing"]).unwrap();
273    }
274
275    #[test]
276    #[should_panic(expected = "Can not find a Record")]
277    fn test_find_record_in_path_non_record() {
278        let mut rec = Record::new();
279        rec.insert("key".to_string(), Value::String("value".to_string()));
280
281        find_record_in_path(&rec, &["key"]).unwrap();
282    }
283
284    #[test]
285    fn test_extract_action_from_mbt_var_success() {
286        let mut rec = Record::new();
287        rec.insert(
288            "mbt::actionTaken".to_string(),
289            Value::String("TestAction".to_string()),
290        );
291
292        let result = extract_action_from_mbt_var(&mut rec).unwrap();
293        assert_eq!(result, "TestAction");
294        assert!(!rec.contains_key("mbt::actionTaken"));
295    }
296
297    #[test]
298    #[should_panic(expected = "Missing `mbt::actionTaken`")]
299    fn test_extract_action_from_mbt_var_missing() {
300        let mut rec = Record::new();
301        extract_action_from_mbt_var(&mut rec).unwrap();
302    }
303
304    #[test]
305    #[should_panic(expected = "Failed to decode `mbt::actionTaken`")]
306    fn test_extract_action_from_mbt_var_wrong_type() {
307        let mut rec = Record::new();
308        rec.insert("mbt::actionTaken".to_string(), Value::Number(42));
309
310        extract_action_from_mbt_var(&mut rec).unwrap();
311    }
312
313    #[test]
314    fn test_extract_nondet_from_mbt_var_success() {
315        let mut rec = Record::new();
316        let nondet_rec = Record::new();
317        rec.insert("mbt::nondetPicks".to_string(), Value::Record(nondet_rec));
318
319        let result = extract_nondet_from_mbt_var(&mut rec);
320        assert!(result.is_ok());
321        assert!(!rec.contains_key("mbt::nondetPicks"));
322    }
323
324    #[test]
325    #[should_panic(expected = "Missing `mbt::nondetPicks`")]
326    fn test_extract_nondet_from_mbt_var_missing() {
327        let mut rec = Record::new();
328        extract_nondet_from_mbt_var(&mut rec).unwrap();
329    }
330
331    #[test]
332    fn test_extract_action_from_sum_type_success() {
333        let mut rec = Record::new();
334        rec.insert("tag".to_string(), Value::String("ActionName".to_string()));
335
336        let result = extract_action_from_sum_type(&rec).unwrap();
337        assert_eq!(result, "ActionName");
338    }
339
340    #[test]
341    #[should_panic(expected = "Expected action to be a sum type variant.")]
342    fn test_extract_action_from_sum_type_missing_tag() {
343        let rec = Record::new();
344        extract_action_from_sum_type(&rec).unwrap();
345    }
346
347    #[test]
348    #[should_panic(expected = "Expected action to be a sum type variant.")]
349    fn test_extract_action_from_sum_type_wrong_type() {
350        let mut rec = Record::new();
351        rec.insert("tag".to_string(), Value::Number(42));
352
353        extract_action_from_sum_type(&rec).unwrap();
354    }
355
356    #[test]
357    fn test_extract_nondet_from_sum_type_empty_tuple() {
358        let mut rec = Record::new();
359        rec.insert("value".to_string(), Value::Tuple(vec![].into()));
360
361        let result = extract_nondet_from_sum_type(&rec).unwrap();
362        assert!(result.is_empty());
363    }
364
365    #[test]
366    fn test_extract_nondet_from_sum_type_record() {
367        let nondet_rec = Record::new();
368        let mut rec = Record::new();
369        rec.insert("value".to_string(), Value::Record(nondet_rec));
370
371        let result = extract_nondet_from_sum_type(&rec);
372        assert!(result.is_ok());
373    }
374
375    #[test]
376    #[should_panic(expected = "Expected nondet picks to be a sum type variant value as a record.")]
377    fn test_extract_nondet_from_sum_type_invalid() {
378        let mut rec = Record::new();
379        rec.insert("value".to_string(), Value::String("invalid".to_string()));
380
381        extract_nondet_from_sum_type(&rec).unwrap();
382    }
383
384    #[test]
385    fn test_extract_from_mbt_vars_success() {
386        let mut rec = Record::new();
387        rec.insert(
388            "mbt::actionTaken".to_string(),
389            Value::String("TestAction".to_string()),
390        );
391
392        let mut nondet_rec = Record::new();
393        nondet_rec.insert("pick1".to_string(), Value::Number(1));
394        rec.insert("mbt::nondetPicks".to_string(), Value::Record(nondet_rec));
395
396        rec.insert(
397            "state_var".to_string(),
398            Value::String("state_value".to_string()),
399        );
400
401        let result = extract_from_mbt_vars(rec, &["state_var"]).unwrap();
402        assert_eq!(result.action_taken, "TestAction");
403        assert_eq!(result.state, Value::String("state_value".to_string()));
404    }
405
406    #[test]
407    #[should_panic(expected = "Missing `mbt::actionTaken`")]
408    fn test_extract_from_mbt_vars_missing_action() {
409        let mut rec = Record::new();
410        let nondet_rec = Record::new();
411        rec.insert("mbt::nondetPicks".to_string(), Value::Record(nondet_rec));
412
413        extract_from_mbt_vars(rec, &[]).unwrap();
414    }
415
416    #[test]
417    fn test_extract_from_sum_type_success() {
418        let mut sum_rec = Record::new();
419        sum_rec.insert("tag".to_string(), Value::String("TestAction".to_string()));
420        sum_rec.insert("value".to_string(), Value::Tuple(vec![].into()));
421
422        let mut rec = Record::new();
423        rec.insert("sum_type".to_string(), Value::Record(sum_rec));
424        rec.insert(
425            "state_var".to_string(),
426            Value::String("state_value".to_string()),
427        );
428
429        let result = extract_from_sum_type(rec, &["sum_type"], &["state_var"]).unwrap();
430        assert_eq!(result.action_taken, "TestAction");
431        assert_eq!(result.state, Value::String("state_value".to_string()));
432        assert!(result.nondet_picks.is_empty());
433    }
434
435    #[test]
436    fn test_extract_from_sum_type_removes_mbt_vars() {
437        let mut sum_rec = Record::new();
438        sum_rec.insert("tag".to_string(), Value::String("TestAction".to_string()));
439        sum_rec.insert("value".to_string(), Value::Tuple(vec![].into()));
440
441        let mut rec = Record::new();
442        rec.insert("sum_type".to_string(), Value::Record(sum_rec));
443        rec.insert(
444            "mbt::actionTaken".to_string(),
445            Value::String("OldAction".to_string()),
446        );
447        rec.insert("mbt::nondetPicks".to_string(), Value::Record(Record::new()));
448        rec.insert(
449            "state_var".to_string(),
450            Value::String("state_value".to_string()),
451        );
452
453        let result = extract_from_sum_type(rec, &["sum_type"], &["state_var"]).unwrap();
454        assert_eq!(result.action_taken, "TestAction");
455    }
456}