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}