Skip to main content

scan/
lib.rs

1//! # SCAN (StatistiCal ANalyzer)
2//!
3//! SCAN is a statistical model checker
4//! designed to verify large concurrent systems
5//! for which standard verification techniques do not scale.
6//!
7//! SCAN uses Channel Systems (CS) as models,[^1]
8//! and Metric Temporal Logic (MTL) as property specification language.
9//!
10//! SCAN is being developed to accept models specified in multiple, rich modeling languages.
11//! At the moment the following languages are planned or implemented:
12//!
13//! - [x] [State Chart XML (SCXML)](https://www.w3.org/TR/scxml/).
14//! - [x] [Promela](https://spinroot.com/spin/Man/Manual.html)
15//! - [x] [JANI](https://jani-spec.org/)
16//!
17//! This crate is part of the [SCAN statistical model checker](https://convince-project.github.io/scan/)
18//!
19//! [^1]: Baier, C., & Katoen, J. (2008). *Principles of model checking*. MIT Press.
20
21#![warn(missing_docs)]
22#![forbid(unsafe_code)]
23
24mod progress;
25mod report;
26mod trace;
27mod verify;
28
29use std::{env::current_dir, path::PathBuf};
30
31use anyhow::{anyhow, bail};
32use clap::{Parser, Subcommand, ValueEnum};
33use progress::Bar;
34use report::Report;
35use scan_core::{Oracle, Scan};
36use trace::TraceArgs;
37use verify::VerifyArgs;
38
39/// Supported model specification formats.
40///
41/// SCAN supports different model specification formats.
42///
43/// WARNING: formats can have varying levels of supports
44/// and may not be interpreted as expected.
45#[deny(missing_docs)]
46#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
47enum Format {
48    /// SCXML format, passed as either the path to the main file,
49    /// if present, or the path of the directory sub-tree containing the model's files.
50    ///
51    /// SCXML models are composed by separate .scxml files for SCXML automaton,
52    /// an .xml file for pMTL properties and,
53    /// optionally, an .xml main file describing the model.
54    ///
55    /// The model can be passed to SCAN as either the path to the main file,
56    /// if present, or the path of a directory.
57    /// In the latter case, SCAN will attempt to process appropriately
58    /// all files in the given directory and its sub-directories.
59    #[cfg(feature = "scxml")]
60    Scxml,
61    /// JANI format, passed as the path to the .jani file.
62    ///
63    /// JANI models are composed by a single .jani file.
64    ///
65    /// The model has to be passed to SCAN as the path to the .jani file.
66    ///
67    /// WARNING: JANI support is still experimental.
68    #[cfg(feature = "jani")]
69    Jani,
70    /// Promela format, passed as the path to the .pml or .prm file.
71    ///
72    /// Promela models are composed by a single .pml or .prm file.
73    ///
74    /// The model has to be passed to SCAN as the path to the .pml or .prm file.
75    ///
76    /// WARNING: Promela support is still experimental.
77    #[cfg(feature = "promela")]
78    Promela,
79}
80
81/// SCAN's available commands.
82#[deny(missing_docs)]
83#[derive(Subcommand)]
84enum Commands {
85    /// Validate the syntactical and semantical correctness of the model, without running it.
86    ///
87    /// Examples:
88    /// 'scan PATH/TO/MODEL validate' validates the model.
89    Validate,
90    /// Verify properties of the given model.
91    ///
92    /// At least one property has to be verified.
93    ///
94    /// Examples:
95    /// 'scan PATH/TO/MODEL verify PROPERTY' verifies the property PROPERTY over the model.
96    /// 'scan PATH/TO/MODEL verify PROPERTY_1 PROPERTY_2' verifies the properties PROPERTY_1 and PROPERTY_2 together over the model.
97    /// 'scan PATH/TO/MODEL verify --all' verifies all specified properties together over the model.
98    #[clap(verbatim_doc_comment)]
99    Verify {
100        /// Args for model verification.
101        #[clap(flatten)]
102        args: VerifyArgs,
103        /// Print progress bars during verification.
104        ///
105        /// By default, when it starts the verification process, SCAN only prints a terse message.
106        /// For longer jobs, it is useful to have real-time feedback on how the verification is proceeding.
107        /// This flag has SCAN print progress bars and current statistics on the verification process,
108        /// and make a best-effort attempt in estimating how long it will take to completion.
109        #[arg(long, value_enum)]
110        progress: Option<Bar>,
111        /// Print JSON-serialized final verification report.
112        ///
113        /// By default, SCAN prints a user-friendly report at the end of verification.
114        /// This flag has the report printed in JSON format instead.
115        #[arg(long)]
116        json: bool,
117    },
118    /// Produce execution traces and save them to file in csv format..
119    ///
120    /// Executions are always run to completion, regardless of verification outcome.
121    /// Executions that verify the given properties are separated from those that do not.
122    /// It is possible to verify no property at all, in which case all executions are successful but still executed to completion.
123    ///
124    /// Examples:
125    /// 'scan PATH/TO/MODEL trace' executes the model once and writes the trace to disk, without verifying any property.
126    /// 'scan PATH/TO/MODEL verify PROPERTY_1 PROPERTY_2' executes the model once and writes the trace to disk, classifying it according to verification outcome of the properties PROPERTY_1 and PROPERTY_2 together over the model.
127    /// 'scan PATH/TO/MODEL verify --all' executes the model once and writes the trace to disk, and classifying it according to verification outcome of all specified properties together over the model.
128    #[clap(verbatim_doc_comment)]
129    Trace(TraceArgs),
130}
131
132const LONG_ABOUT: &str = "SCAN (StatistiCal ANalyzer) is a statistical model checker \
133designed to verify large concurrent systems \
134for which standard verification techniques do not scale.";
135
136/// SCAN (StatistiCal ANalyzer) is a statistical model checker
137/// designed to verify large concurrent systems
138/// for which standard verification techniques do not scale.
139#[derive(Parser)]
140#[deny(missing_docs)]
141#[command(version, about, long_about=LONG_ABOUT)]
142pub struct Cli {
143    /// Path of model's file or folder,
144    /// depending on the used specification format.
145    #[arg(value_hint = clap::ValueHint::AnyPath)]
146    model: PathBuf,
147    /// Format used to specify the model.
148    ///
149    /// SCAN supports different model specification formats,
150    /// and, by default, it attempts to autodetect the correct one,
151    /// but this can be specified to resolve ambiguity.
152    ///
153    /// WARNING: formats can have varying levels of supports
154    /// and may not be interpreted as expected.
155    #[arg(short, long, value_enum)]
156    format: Option<Format>,
157    /// Verbose output.
158    ///
159    /// Verbosity level corresponds to the log level that gets printed.
160    /// Logging mostly concerns parsing and building of the model,
161    /// so it is particularly useful when validating a model
162    /// to check for errors.
163    ///
164    /// Verbosity levels: ERROR, WARNING, INFO, DEBUG, TRACE
165    ///
166    /// Examples:
167    /// 'scan PATH/TO/MODEL validate -vvv' validates the model and prints ERROR, WARNING and INFO log entries.
168    #[command(flatten)]
169    pub verbosity: clap_verbosity_flag::Verbosity,
170    /// Actions to execute on the model.
171    #[command(subcommand)]
172    command: Commands,
173}
174
175impl Cli {
176    /// Run SCAN with the parameters passed via the CLI.
177    pub fn run(self) -> anyhow::Result<()> {
178        let model = std::path::absolute(&self.model)?
179            .file_name()
180            .and_then(std::ffi::OsStr::to_str)
181            .unwrap_or("model")
182            .to_owned();
183
184        if let Some(format) = self.format {
185            match format {
186                #[cfg(feature = "scxml")]
187                Format::Scxml => self.run_scxml(&model),
188                #[cfg(feature = "jani")]
189                Format::Jani => self.run_jani(&model),
190                #[cfg(feature = "promela")]
191                Format::Promela => self.run_promela(&model),
192            }
193        } else if self.model.is_dir() {
194            self.run_scxml(&model)
195        } else {
196            let ext = self
197                .model
198                .extension()
199                .ok_or(anyhow!("file extension unknown"))?;
200            match ext
201                .to_str()
202                .ok_or(anyhow!("file extension not recognized"))?
203            {
204                #[cfg(feature = "scxml")]
205                "xml" | "scxml" => self.run_scxml(&model),
206                #[cfg(feature = "jani")]
207                "jani" => self.run_jani(&model),
208                #[cfg(feature = "promela")]
209                "pml" | "prm" => self.run_promela(&model),
210                _ => bail!("unsupported file format"),
211            }
212        }
213    }
214
215    #[cfg(feature = "scxml")]
216    fn run_scxml(self, model: &str) -> anyhow::Result<()> {
217        use scan_scxml::*;
218
219        match self.command {
220            Commands::Verify {
221                mut args,
222                progress,
223                json,
224            } => {
225                args.validate()?;
226                let (scan_def, scxml_model) = load(&self.model, &args.properties, args.all)?;
227                validate_properties(&args.properties, &scxml_model.guarantees)?;
228                // Reorder properties as they appear in the model
229                args.properties = scxml_model.guarantees.clone();
230                run_verification::<_>(model, &args, progress, json, &scan_def).print(json);
231            }
232            Commands::Validate => {
233                let (_scan, _scxml_model) = load(&self.model, &[], true)?;
234                // At this point the model has been validated
235                println!("model '{model}' successfully validated");
236            }
237            Commands::Trace(mut args) => {
238                let (scan_def, scxml_model) = load(&self.model, &args.properties, args.all)?;
239                validate_properties(&args.properties, &scxml_model.guarantees)?;
240                // Reorder properties as they appear in the model
241                args.properties = scxml_model.guarantees.clone();
242                let path = new_traces_dir();
243                args.trace::<_, TracePrinter>(&scan_def, path, &scxml_model);
244                println!("trace computation for model '{model}' completed");
245            }
246        }
247        Ok(())
248    }
249
250    #[cfg(feature = "jani")]
251    fn run_jani(self, model: &str) -> anyhow::Result<()> {
252        use scan_jani::*;
253
254        match self.command {
255            Commands::Verify {
256                mut args,
257                progress,
258                json,
259            } => {
260                args.validate()?;
261                let properties = args.properties.clone();
262                let (scan, jani_model) = load(&self.model, &properties)?;
263                validate_properties(&args.properties, &jani_model.guarantees)?;
264                // Reorder properties as they appear in the model
265                args.properties = jani_model.guarantees.clone();
266                run_verification::<_>(model, &args, progress, json, &scan).print(json);
267            }
268            Commands::Validate => {
269                let (_scan, _jani_model) = load(&self.model, &[])?;
270                println!("model '{model}' successfully validated");
271            }
272            Commands::Trace(args) => {
273                args.validate()?;
274                let (scan, jani_model) = load(&self.model, &[])?;
275                let path = new_traces_dir();
276                args.trace::<_, TracePrinter>(&scan, path, &jani_model);
277                println!("trace computation for model '{model}' completed");
278            }
279        }
280        Ok(())
281    }
282
283    #[cfg(feature = "promela")]
284    fn run_promela(self, model: &str) -> anyhow::Result<()> {
285        use scan_promela::*;
286
287        match self.command {
288            Commands::Verify {
289                args,
290                progress,
291                json,
292            } => {
293                args.validate()?;
294                let properties = args.properties.clone();
295                let (scan, _promela_model) = load(&self.model, &properties, args.all)?;
296                run_verification::<_>(model, &args, progress, json, &scan).print(json);
297            }
298            Commands::Validate => {
299                let (_scan, _jani_model) = load(&self.model, &[], true)?;
300                println!("model '{model}' successfully validated");
301            }
302            Commands::Trace(args) => {
303                args.validate()?;
304                let (_scan, _promela_model) = load(&self.model, &[], args.all)?;
305                println!("processing model '{model}' completed");
306            }
307        }
308        Ok(())
309    }
310}
311
312fn validate_properties(props: &[String], all_props: &[String]) -> anyhow::Result<()> {
313    if let Some(prop) = props.iter().find(|prop| !all_props.contains(prop)) {
314        Err(anyhow!(
315            "no property named '{prop}' found in model.\n\nHint: maybe it is misspelled?"
316        ))
317    } else {
318        Ok(())
319    }
320}
321
322fn run_verification<'a, O>(
323    model: &str,
324    args: &VerifyArgs,
325    progress: Option<Bar>,
326    json: bool,
327    scan: &'a Scan<O>,
328) -> Report
329where
330    O: 'a + Oracle + Clone + Sync,
331{
332    if !json {
333        println!(
334            "Verifying {model} (-p {} -c {}) {:?}",
335            args.precision, args.confidence, args.properties
336        );
337    }
338    if let Some(bar) = progress {
339        std::thread::scope(|s| {
340            s.spawn(|| {
341                bar.print_progress_bar::<O>(
342                    args.confidence,
343                    args.precision,
344                    &args.properties,
345                    scan,
346                );
347            });
348            args.verify::<O>(model.to_owned(), scan)
349        })
350    } else {
351        args.verify::<O>(model.to_owned(), scan)
352    }
353}
354
355fn new_traces_dir() -> std::path::PathBuf {
356    const FOLDER: &str = "traces";
357
358    let mut path = current_dir().expect("current dir");
359    for i in 0.. {
360        path.push(format!("{}_{i:02}", FOLDER));
361        if std::fs::exists(&path).is_ok_and(|exists| !exists) {
362            break;
363        } else {
364            assert!(path.pop());
365        }
366    }
367    path
368}
369
370// From Clap tutorial <https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#testing>
371#[test]
372fn verify_cli() {
373    use clap::CommandFactory;
374    Cli::command().debug_assert();
375}