taco_cli/
cli.rs

1//! Command Line Interface for TACO
2//!
3//! TACO uses the `clap` crate to parse command line arguments and create the
4//! CLI interface. This module defines all available commands and options (and
5//! their documentation) as well as some utility functions to apply these
6//! options.
7
8use std::result::Result::Ok;
9use std::{fmt::Display, fs, io, path::PathBuf, process::exit};
10
11use anyhow::{Context, anyhow};
12
13use clap::{Args, Parser, Subcommand, ValueEnum};
14use taco_bdd::BDDManagerConfig;
15
16use log::{LevelFilter, error, info};
17use log4rs::{
18    Config,
19    append::console::ConsoleAppender,
20    config::{Appender, Root},
21    encode::pattern::PatternEncoder,
22};
23use taco_acs_model_checker::ACSModelChecker;
24use taco_display_utils::join_iterator;
25use taco_model_checker::preprocessing::ExistingPreprocessors;
26use taco_model_checker::{
27    ModelChecker,
28    eltl::{ELTLExpression, ELTLSpecification},
29    preprocessing::{self},
30};
31use taco_model_checker::{ModelCheckerContext, ModelCheckerResult, TargetSpec};
32use taco_parser::{ParseTAWithLTL, bymc::ByMCParser, tla::TLAParser};
33use taco_smt_encoder::{ProvidesSMTSolverBuilder, SMTSolverBuilderCfg};
34
35use taco_smt_model_checker::{SMTModelChecker, SMTModelCheckerOptions};
36use taco_threshold_automaton::{
37    ModifiableThresholdAutomaton, ThresholdAutomaton,
38    expressions::{Atomic, BooleanExpression, ComparisonOp, IntegerExpression, Location, Variable},
39    general_threshold_automaton::GeneralThresholdAutomaton,
40};
41use taco_zcs_model_checker::{ZCSModelChecker, ZCSModelCheckerHeuristics};
42
43use crate::cli::output_formats::{into_bymc, into_tla};
44use crate::{cli, taco_config::TACOConfig};
45
46mod output_formats;
47
48/// Preprocessors that will be used by default
49pub const DEFAULT_PREPROCESSORS: [ExistingPreprocessors; 5] = [
50    ExistingPreprocessors::DropSelfLoops,
51    ExistingPreprocessors::DropUnreachableLocations,
52    ExistingPreprocessors::RemoveUnusedVariables,
53    ExistingPreprocessors::CheckInitCondSatSMT,
54    ExistingPreprocessors::ReplaceTrivialGuardsSMT,
55];
56
57/// TACO toolsuite for threshold automata  - Command Line Interface
58///
59/// This is the command line interface for the TACO toolsuite for threshold
60/// automata. You can use the --help / -h flag to get all available commands and
61/// options.
62/// For more advanced configuration options, an introduction into the different
63/// specification formats and an introduction into the formal model of threshold
64/// automata, visit the TACO homepage:
65///
66///     https://taco-mc.dev
67///
68/// If you have any questions or you encounter bugs, feel free to open an issue
69/// on TACO's GitHub repository:
70///
71///     https://github.com/cispa/TACO/issues/new/choose
72///
73///
74#[derive(Parser, Debug)]
75#[command(version, name = "TACO CLI", about, long_about)]
76pub(crate) struct Cli {
77    #[command(flatten)]
78    pub(crate) log_config: LoggerConfig,
79    #[command(subcommand)]
80    pub(crate) command: Commands,
81}
82
83#[derive(Subcommand, Debug)]
84pub enum Commands {
85    /// Read the specification file and check the specified properties
86    Check {
87        #[command(flatten)]
88        input: SpecFileInput,
89
90        /// Configuration file for the model checker
91        #[arg(short, long, value_name = "CONFIG_FILE")]
92        config_file: Option<PathBuf>,
93
94        /// Request a specific model checker to be used
95        #[command(subcommand)]
96        model_checker: Option<ModelCheckerOption>,
97
98        /// Configure the BDD library to use
99        #[arg(short, long, value_name = "BDD")]
100        bdd: Option<BDDManagerOption>,
101
102        /// Select the SMT solver to be used
103        #[arg(short, long, value_name = "SMT_SOLVER")]
104        smt_solver: Option<SMTSolverDefaultOptions>,
105
106        /// Enable compact output of error paths omitting locations with 0
107        /// processes and variables that are 0
108        #[arg(short = 'o', long, default_value_t = false)]
109        compact_out: bool,
110
111        /// Abort the model checking on the first counter example that has been
112        /// found
113        #[arg(short, long, default_value_t = false)]
114        abort_on_violation: bool,
115
116        /// Choose the mode with which to run the check
117        #[arg(long, value_enum, default_value_t = CheckMode::Normal)]
118        mode: CheckMode,
119    },
120    #[cfg(feature = "dot")]
121    /// Read the specification file and visualize the threshold automaton
122    Visualize {
123        #[command(flatten)]
124        input: SpecFileInput,
125
126        #[command(flatten)]
127        output: VisualizationOutput,
128    },
129    /// Translate from one specification file format to another one
130    Translate {
131        #[command(flatten)]
132        input: SpecFileInput,
133
134        #[command(flatten)]
135        output: TranslationOutput,
136    },
137}
138
139#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
140pub enum CheckMode {
141    /// Normal model checking, given the ta and spec from the input file
142    /// (default)
143    Normal,
144    /// Coverability check through the CLI
145    Coverability,
146    /// Reachability check through the CLI
147    Reachability,
148}
149
150#[derive(Args, Debug)]
151pub(crate) struct SpecFileInput {
152    /// Location and name of the specification file
153    input_file: PathBuf,
154
155    /// Format of the input specification file (if the file ending is not `.ta`,
156    /// `.eta`, `.tla`)
157    #[arg(short, long, value_name = "INPUT_FORMAT")]
158    format: Option<SpecFileFormat>,
159}
160
161#[allow(clippy::upper_case_acronyms)]
162#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
163enum SpecFileFormat {
164    /// Specification file format introduced by ByMC (usually `.ta` or `.eta` files)
165    BYMC,
166    /// TLA+ specification (usually `.tla` files)
167    TLA,
168}
169
170impl Display for SpecFileFormat {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            SpecFileFormat::BYMC => write!(f, "ByMC"),
174            SpecFileFormat::TLA => write!(f, "TLA+"),
175        }
176    }
177}
178
179#[derive(Subcommand, Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
180pub(crate) enum ModelCheckerOption {
181    /// Symbolic model checker
182    Zcs {
183        /// Select a heuristics for the symbolic model checker
184        #[arg(long, value_name = "HEURISTICS")]
185        heuristic: Option<SymbolicModelCheckerHeuristics>,
186    },
187    /// SMT based model checker
188    Smt {
189        /// Enable parallel model checking (default: false / off)
190        #[arg(short, long, value_name = "PARALLEL", default_value_t = false)]
191        parallel: bool,
192    },
193    /// Counter system based model checker
194    Acs,
195}
196
197#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
198pub(crate) enum SymbolicModelCheckerHeuristics {
199    /// This heuristics yields a semi-decision procedure by unfolding cycles if
200    /// the input TA contains resets but no decrements. It can be used to verify
201    /// extended threshold automata for coverability or reachability
202    /// specifications.
203    ResetHeuristics,
204    /// This heuristics yields a semi-decision procedure by unfolding cycles if
205    /// the input TA contains increments and decrements. It can be used to
206    /// verify extended threshold automata for coverability or reachability
207    /// specifications.
208    DecrementAndIncrementHeuristics,
209    /// This heuristics yields a decision procedure by only traversing cycles
210    /// once. It can be used to verify canonical threshold automata (no
211    /// resets/decrements) for coverability or reachability specifications.
212    CanonicalHeuristics,
213    /// This is a greedy heuristics that only checks if the symbolic error graph
214    /// is empty. It is neither complete nor sound but guarantees termination.
215    /// If the error graph is empty the property holds, otherwise no conclusion
216    /// can be drawn.
217    EmptyErrorGraphHeuristic,
218}
219
220#[allow(clippy::upper_case_acronyms)]
221#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
222pub(crate) enum BDDManagerOption {
223    /// CUDD BDD library
224    #[cfg(feature = "cudd")]
225    CUDD,
226    /// OxiDD BDD library
227    #[cfg(feature = "oxidd")]
228    OXIDD,
229}
230
231#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
232/// SMT solvers that are supported by default
233pub enum SMTSolverDefaultOptions {
234    /// Z3 SMT solver
235    Z3,
236    /// CVC5 SMT solver
237    CVC5,
238}
239
240#[derive(Debug, Args)]
241pub struct VisualizationOutput {
242    /// Output file to save the visualization
243    output_file: PathBuf,
244    /// Output format for the visualization
245    ///
246    /// Supported formats are: `dot`, `svg`, `png`; default is `dot`
247    /// Note that `svg` and `png` formats require the `graphviz` library to be
248    /// installed on the system
249    #[arg(short, long, value_name = "OUT_FORMAT", default_value = "dot")]
250    output_format: OutputFormat,
251}
252
253#[allow(clippy::upper_case_acronyms)]
254#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
255pub enum OutputFormat {
256    /// Output as DOT
257    DOT,
258    /// Output as SVG
259    SVG,
260    /// Output as PNG
261    PNG,
262}
263
264#[derive(Debug, Args)]
265pub struct TranslationOutput {
266    /// Output file to save the translated output to
267    output_file: PathBuf,
268    /// Format to translate to (default: ByMC)
269    #[arg(short, long, value_name = "OUT_FORMAT", default_value = "bymc")]
270    output_format: SpecFileFormat,
271}
272
273#[derive(Debug, Args)]
274pub(crate) struct LoggerConfig {
275    /// Read the logger configuration from file.
276    /// Logger configuration can be provided in the log4rs specification format.
277    #[arg(long)]
278    logger_config_file: Option<String>,
279
280    /// Enable debug output.
281    /// **Note**: This flag must be passed first, before any command.
282    #[arg(short, long, default_value_t = false)]
283    debug: bool,
284}
285
286/// Initialize the logger as specified in `cfg`
287///
288/// By default the logger is configured to log to stdout. If a log4rs
289/// configuration file is given in `cfg`, the configuration from that file will
290/// be used instead
291pub(crate) fn initialize_logger(cfg: LoggerConfig) -> Result<(), anyhow::Error> {
292    if let Some(f) = cfg.logger_config_file {
293        // Read logger configuration file
294        log4rs::init_file(f, Default::default())
295            .with_context(|| "Failed to read logger config file")?;
296        return Ok(());
297    }
298
299    let p_encoder = match cfg.debug {
300        true => PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S)} - {h({l})} - [{f}:{L} - {M}] - {m}{n}"),
301        false => PatternEncoder::new("{d(%H:%M:%S)} - {h({l})} - {m}{n}"),
302    };
303
304    // Log to stdout
305    let stdout = ConsoleAppender::builder()
306        .encoder(Box::new(p_encoder))
307        .build();
308
309    let mut level = LevelFilter::Info;
310    if cfg.debug {
311        level = LevelFilter::Debug;
312    }
313
314    let log_config = Config::builder()
315        .appender(Appender::builder().build("stdout", Box::new(stdout)))
316        .build(Root::builder().appender("stdout").build(level))
317        .expect("Failed to initialize logger");
318
319    log4rs::init_config(log_config).expect("Failed to initialize console logger");
320    Ok(())
321}
322
323/// Get SMT solver configuration based on selected solver
324pub(crate) fn get_smt_solver(smt_config: SMTSolverDefaultOptions) -> SMTSolverBuilderCfg {
325    match smt_config {
326        SMTSolverDefaultOptions::Z3 => SMTSolverBuilderCfg::new_z3(),
327        SMTSolverDefaultOptions::CVC5 => SMTSolverBuilderCfg::new_cvc5(),
328    }
329}
330
331/// Get BDD manager configuration based on selected BDD manager
332pub(crate) fn get_bdd_manager_cfg(bdd: BDDManagerOption) -> BDDManagerConfig {
333    match bdd {
334        #[cfg(feature = "cudd")]
335        BDDManagerOption::CUDD => BDDManagerConfig::new_cudd(),
336        #[cfg(feature = "oxidd")]
337        BDDManagerOption::OXIDD => BDDManagerConfig::new_oxidd(),
338    }
339}
340
341/// Get the symbolic model checker heuristic based on the CLI option
342fn get_zcs_model_checker_heuristic(
343    heuristic: Option<SymbolicModelCheckerHeuristics>,
344) -> Option<ZCSModelCheckerHeuristics> {
345    heuristic.map(|h| match h {
346        SymbolicModelCheckerHeuristics::ResetHeuristics => {
347            ZCSModelCheckerHeuristics::ResetHeuristics
348        }
349        SymbolicModelCheckerHeuristics::DecrementAndIncrementHeuristics => {
350            ZCSModelCheckerHeuristics::DecrementAndIncrementHeuristics
351        }
352        SymbolicModelCheckerHeuristics::CanonicalHeuristics => {
353            ZCSModelCheckerHeuristics::CanonicalHeuristics
354        }
355        SymbolicModelCheckerHeuristics::EmptyErrorGraphHeuristic => {
356            ZCSModelCheckerHeuristics::EmptyErrorGraphHeuristics
357        }
358    })
359}
360
361/// Constructs the preprocessors, either from the configuration file or just
362/// uses the default preprocessors specified in [DEFAULT_PREPROCESSORS]
363fn get_preprocessors<S, C>(
364    cfg: &TACOConfig,
365) -> Vec<Box<dyn preprocessing::Preprocessor<GeneralThresholdAutomaton, S, C>>>
366where
367    S: TargetSpec,
368    C: ModelCheckerContext + ProvidesSMTSolverBuilder,
369{
370    if cfg.get_preprocessors_cfg().is_some() {
371        info!(
372            "Configured TACO to use preprocessors: {}",
373            join_iterator(cfg.get_preprocessors_cfg().clone().unwrap().iter(), ", ")
374        );
375    }
376
377    cfg.get_preprocessors_cfg()
378        .clone()
379        .unwrap_or(DEFAULT_PREPROCESSORS.to_vec())
380        .into_iter()
381        .map(|mc| mc.into())
382        .collect()
383}
384
385/// Initialize the symbolic model checker with the given options,
386/// threshold automaton and specifications
387pub(crate) fn initialize_zcs_model_checker(
388    mc_cfg: Option<SymbolicModelCheckerHeuristics>,
389    ta: GeneralThresholdAutomaton,
390    spec: ELTLSpecification,
391    cfg: TACOConfig,
392) -> ZCSModelChecker {
393    let opt = get_zcs_model_checker_heuristic(mc_cfg);
394
395    let preprocessors = get_preprocessors(&cfg);
396
397    let mc = ZCSModelChecker::new(
398        Some(cfg.get_bdd_smt_config()),
399        opt,
400        preprocessors,
401        ta,
402        spec.into_iter(),
403    );
404
405    if let Err(err) = mc {
406        error!("Error during initialization of the model checker: {}", err);
407        exit(1);
408    }
409
410    mc.unwrap()
411}
412
413pub(crate) fn initialize_smt_model_checker(
414    ta: GeneralThresholdAutomaton,
415    spec: ELTLSpecification,
416    cfg: TACOConfig,
417    parallel: bool,
418) -> SMTModelChecker {
419    let opts = SMTModelCheckerOptions::new(parallel);
420
421    let preprocessors = get_preprocessors(&cfg);
422
423    let mc = SMTModelChecker::new(
424        cfg.get_smt_solver_builder_cfg(),
425        opts,
426        preprocessors,
427        ta,
428        spec.into_iter(),
429    );
430
431    if let Err(err) = mc {
432        error!("Error during initialization of the model checker: {}", err);
433        exit(1);
434    }
435
436    mc.unwrap()
437}
438
439pub(crate) fn initialize_cs_model_checker(
440    ta: GeneralThresholdAutomaton,
441    spec: ELTLSpecification,
442    cfg: TACOConfig,
443) -> ACSModelChecker {
444    let preprocessors = get_preprocessors(&cfg);
445
446    let mode = None;
447
448    let mc = ACSModelChecker::new(
449        cfg.get_smt_solver_builder_cfg(),
450        mode,
451        preprocessors,
452        ta,
453        spec.into_iter(),
454    );
455
456    if let Err(err) = mc {
457        error!("Error during initialization of the model checker: {}", err);
458        exit(1);
459    }
460
461    mc.unwrap()
462}
463
464/// Parse the input file into a threshold automaton
465///
466/// This tries to open the file given by `SpecFileInput` and tries to format it
467/// according to the format specified. If no format is specified, tries to
468/// detect the file ending, otherwise uses ByMC by default.
469pub(crate) fn parse_input_file(
470    cfg: SpecFileInput,
471) -> Result<(GeneralThresholdAutomaton, ELTLSpecification), anyhow::Error> {
472    let mut format = cfg.format;
473
474    if format.is_none()
475        && let Some(ext) = cfg.input_file.extension()
476    {
477        let ext = ext.to_str().unwrap();
478        if ext == "ta" || ext == "eta" {
479            format = Some(SpecFileFormat::BYMC);
480        }
481        if ext == "tla" {
482            format = Some(SpecFileFormat::TLA);
483        }
484    }
485
486    let f =
487        fs::read_to_string(cfg.input_file).with_context(|| "Unable to read specification file")?;
488
489    match format {
490        None | Some(SpecFileFormat::BYMC) => ByMCParser::new().parse_ta_and_spec(&f),
491        Some(SpecFileFormat::TLA) => TLAParser::new().parse_ta_and_spec(&f),
492    }
493}
494
495/// Visualize the threshold automaton in the given format
496///
497/// When `svg` or `png` format is selected, the `graphviz` library must be
498/// installed on the system.
499#[cfg(feature = "dot")]
500pub(crate) fn visualize_ta(
501    ta: &GeneralThresholdAutomaton,
502    cfg: VisualizationOutput,
503) -> Result<(), anyhow::Error> {
504    use std::{
505        borrow::BorrowMut,
506        process::{Command, Stdio},
507    };
508
509    use anyhow::Context;
510    use taco_threshold_automaton::dot::ToDOT;
511    let out_str = ta.get_dot_graph();
512
513    let out_arg = match cfg.output_format {
514        OutputFormat::DOT => {
515            fs::write(cfg.output_file, out_str).with_context(|| "Failed to write output file")?;
516            return Ok(());
517        }
518        OutputFormat::SVG => "-Tsvg",
519        OutputFormat::PNG => "-Tpng",
520    };
521
522    if Command::new("dot")
523        .arg("--version")
524        .stdout(Stdio::null())
525        .stderr(Stdio::null())
526        .spawn()
527        .is_err()
528    {
529        return Err(anyhow!("Graphviz is not installed on the system"));
530    }
531
532    let mut echo_cmd = Command::new("echo")
533        .arg(out_str)
534        .stdout(Stdio::piped())
535        .spawn()
536        .unwrap();
537
538    let _ = echo_cmd.borrow_mut().wait();
539
540    let _ = Command::new("dot")
541        .stdin(echo_cmd.stdout.unwrap())
542        .arg(out_arg)
543        .arg("-o")
544        .arg(cfg.output_file)
545        .output()
546        .with_context(|| "Failed to execute graphviz")?;
547
548    Ok(())
549}
550
551fn model_check_ta(
552    ta: GeneralThresholdAutomaton,
553    spec: ELTLSpecification,
554    model_checker: Option<ModelCheckerOption>,
555    config: TACOConfig,
556    abort_on_violation: bool,
557) -> ModelCheckerResult {
558    match model_checker.unwrap_or_else(|| {
559        if ta.has_resets() || ta.has_decrements() {
560            return ModelCheckerOption::Zcs { heuristic: None };
561        }
562
563        ModelCheckerOption::Smt { parallel: false }
564    }) {
565        cli::ModelCheckerOption::Zcs { heuristic } => {
566            let res = initialize_zcs_model_checker(heuristic, ta, spec, config)
567                .verify(abort_on_violation);
568
569            if let Err(err) = res {
570                error!(
571                    "An error occurred during the model checking with the symbolic model checker: {}",
572                    err
573                );
574                exit(1);
575            }
576
577            res.unwrap()
578        }
579        cli::ModelCheckerOption::Smt { parallel } => {
580            let res =
581                initialize_smt_model_checker(ta, spec, config, parallel).verify(abort_on_violation);
582            if let Err(err) = res {
583                error!(
584                    "An error occurred during the model checking with the SMT model checker: {}",
585                    err
586                );
587                exit(1)
588            }
589
590            res.unwrap()
591        }
592        cli::ModelCheckerOption::Acs => {
593            let res = initialize_cs_model_checker(ta, spec, config).verify(abort_on_violation);
594
595            if let Err(err) = res {
596                error!(
597                    "An error occurred during the model checking with the CS model checker: {}",
598                    err
599                );
600                exit(1)
601            }
602
603            res.unwrap()
604        }
605    }
606}
607
608pub fn display_result(
609    ta: GeneralThresholdAutomaton,
610    spec: ELTLSpecification,
611    model_checker: Option<ModelCheckerOption>,
612    config: TACOConfig,
613    abort_on_violation: bool,
614    mode: CheckMode,
615    compact_out: bool,
616) {
617    let res = model_check_ta(ta, spec, model_checker, config, abort_on_violation);
618
619    match mode {
620        // If we find a counter-example for the specification [](negated goal configuration) for coverability or reachability,
621        // We return this as the path that we were looking for.
622        cli::CheckMode::Coverability | cli::CheckMode::Reachability => match res {
623            taco_model_checker::ModelCheckerResult::SAFE => {
624                info!(
625                    "The provided configuration is not reachable from the initial configuration. Coverability/Reachability property not satisfied."
626                )
627            }
628            taco_model_checker::ModelCheckerResult::UNSAFE(violations) => {
629                for (property, error_path) in violations {
630                    if compact_out {
631                        info!(
632                            "Path towards the desired configuration of our property: '{property}': {}",
633                            error_path.display_compact()
634                        );
635                    } else {
636                        info!(
637                            "Path towards the desired configuration of our property: '{property}': {error_path}\n"
638                        )
639                    }
640                }
641            }
642            taco_model_checker::ModelCheckerResult::UNKNOWN(unknown) => {
643                for property in unknown {
644                    info!(
645                        "The configured model checker could not determine whether the threshold automaton satisfies property: {property}"
646                    );
647                }
648            }
649        },
650        _ => match res {
651            taco_model_checker::ModelCheckerResult::SAFE => {
652                info!("Threshold automaton satisfies all properties. The protocol is safe.")
653            }
654            taco_model_checker::ModelCheckerResult::UNSAFE(violations) => {
655                for (property, error_path) in violations {
656                    if compact_out {
657                        info!(
658                            "Counter example to property '{property}': {}",
659                            error_path.display_compact()
660                        );
661                    } else {
662                        info!("Counter example to property '{property}': {error_path}\n")
663                    }
664                }
665                info!("The protocol is unsafe.")
666            }
667            taco_model_checker::ModelCheckerResult::UNKNOWN(unknown) => {
668                for property in unknown {
669                    info!(
670                        "The configured model checker could not determine whether the threshold automaton satisfies property: {property}"
671                    );
672                }
673            }
674        },
675    }
676}
677
678pub fn parse_coverability(ta: &mut GeneralThresholdAutomaton) -> String {
679    let mut loc_name = String::new();
680    loop {
681        println!(
682            "Please input the name of the location that should be covered in your coverability check:"
683        );
684        loc_name.clear();
685        io::stdin().read_line(&mut loc_name).unwrap();
686
687        // Check whether this location is actually in the ta
688        if ta
689            .locations()
690            .any(|l| l.name() == Location::new(loc_name.trim()).name())
691        {
692            break;
693        }
694        println!(
695            "Location '{}' does not exist in the threshold automaton!",
696            loc_name.trim()
697        );
698        let all_locations: Vec<&str> = ta.locations().map(|l| l.name()).collect();
699        println!("Possible locations are : {}", all_locations.join(", "))
700    }
701    // Ask for the initial locations
702    parse_initial_configuration(ta);
703
704    println!(
705        "Checking whether location '{}' can be covered in the threshold automaton.",
706        loc_name
707    );
708    loc_name
709}
710
711/// The coverability specification looks as follows:
712/// We want to find a path from an initial configuration to a configuration where the location is covered
713/// Thus the specification looks as follows:
714/// (expression_initial) => [](to_be_covered_loc = 0)
715/// Since the initial configuration is coded into the ta, there is no need for the implication.
716/// A counter example to this specification is exactly the path we are looking for
717pub fn create_coverability_expression(loc_name: String) -> ELTLExpression {
718    ELTLExpression::Globally(Box::new(ELTLExpression::LocationExpr(
719        Box::new(IntegerExpression::Atom(Location::new(loc_name.trim()))),
720        ComparisonOp::Eq,
721        Box::new(IntegerExpression::Const(0)),
722    )))
723}
724
725pub fn parse_reachability(ta: &mut GeneralThresholdAutomaton) -> (Vec<String>, Vec<String>) {
726    // Ask for the reachability specification
727    let mut populated_locs: Vec<String> = Vec::new();
728    loop {
729        println!(
730            "Input the name of a location that should be covered by processes or press enter when you have input all of them."
731        );
732        let mut loc_name = String::new();
733        io::stdin().read_line(&mut loc_name).unwrap();
734
735        // Check at least one location has been input
736        if populated_locs.is_empty() && loc_name.trim().is_empty() {
737            println!("Please input at least one location that should be populated.");
738            continue;
739        }
740        // Leave the input loop when enter was pressed and at least one valid location was input
741        if loc_name.trim().is_empty() {
742            break;
743        }
744
745        // Check that the location hasn't been input before
746        if populated_locs.contains(&loc_name.trim().to_string()) {
747            println!("You have already specified this location.");
748            continue;
749        }
750        // Check that the location is actually in the ta
751        if !ta
752            .locations()
753            .any(|l| l.name() == Location::new(loc_name.trim()).name())
754        {
755            println!(
756                "Location '{}' does not exist in the threshold automaton!",
757                loc_name.trim()
758            );
759            let all_locations: Vec<&str> = ta.locations().map(|l| l.name()).collect();
760            println!("Possible locations are : {}", all_locations.join(", "));
761            continue;
762        }
763
764        populated_locs.push(loc_name.trim().to_string());
765    }
766
767    // Compute the unpopulated locations
768    let unpopulated_locs: Vec<String> = ta
769        .locations()
770        .filter(|l| !populated_locs.contains(&l.name().to_string()))
771        .map(|name| name.to_string())
772        .collect();
773
774    // Ask for the initial configuration
775    parse_initial_configuration(ta);
776
777    println!(
778        "Checking whether the specified configuration can be reached in the threshold automaton. The configuration looks as follows: locations to be populated: {}, and locations to be unpopulated: {}",
779        populated_locs.join(", "),
780        unpopulated_locs.join(", ")
781    );
782    (populated_locs, unpopulated_locs)
783}
784
785/// The reachability specification looks as follows:
786/// We want to find a path from an initial configuration to a specific configuration
787/// Thus the specification looks as follows:
788/// (expression_initial) =>
789/// []((pop_loc_1 = 0) || ... || (pop_loc_p = 0) || (unpop_loc_1 > 0) || ... || (unpop_loc_u > 0)
790/// Since the initial configuration is coded into the ta, there is no need for the implication.
791/// A counter example to this specification is exactly the path we are looking for
792pub fn create_reachability_expression(
793    populated_locs: Vec<String>,
794    unpopulated_locs: Vec<String>,
795) -> ELTLExpression {
796    let populated_expression = populated_locs
797        .iter()
798        .map(|v| {
799            Box::new(ELTLExpression::LocationExpr(
800                Box::new(IntegerExpression::Atom(Location::new(v))),
801                ComparisonOp::Eq,
802                Box::new(IntegerExpression::Const(0)),
803            ))
804        })
805        .reduce(|acc, val| Box::new(ELTLExpression::And(acc, val)))
806        .unwrap();
807
808    let unpopulated_expression = unpopulated_locs
809        .iter()
810        .map(|v| {
811            Box::new(ELTLExpression::LocationExpr(
812                Box::new(IntegerExpression::Atom(Location::new(v))),
813                ComparisonOp::Gt,
814                Box::new(IntegerExpression::Const(0)),
815            ))
816        })
817        .reduce(|acc, val| Box::new(ELTLExpression::And(acc, val)))
818        .unwrap();
819
820    ELTLExpression::Globally(Box::new(ELTLExpression::Or(
821        populated_expression,
822        unpopulated_expression,
823    )))
824}
825
826fn parse_initial_configuration(ta: &mut GeneralThresholdAutomaton) {
827    println!(
828        "Do you want to manually input an initial configuration? [y/n] If not, the initial locations and the initial variable valuations specified in the provided .ta file will be used."
829    );
830    let mut input = String::new();
831    io::stdin().read_line(&mut input).unwrap();
832    let input = input.trim().to_lowercase();
833
834    if !(input == "y" || input == "yes") {
835        println!(
836            "Using the initial locations and initial variable constraints provided in the .ta file."
837        );
838        return;
839    }
840
841    println!(
842        "Please input the number of processes in each location for your initial configuration:"
843    );
844    let mut location_constraints = Vec::new();
845    for loc in ta.locations() {
846        let procs_in_loc = loop {
847            println!("{}:", loc.name());
848            let mut num = String::new();
849            io::stdin().read_line(&mut num).unwrap();
850            match num.trim().parse::<u32>() {
851                Ok(num) => break num,
852                _ => println!("Invalid input. Please enter a number greater or equal to 0."),
853            }
854        };
855        location_constraints.push(BooleanExpression::ComparisonExpression(
856            Box::new(IntegerExpression::Atom(Location::new(loc.name()))),
857            ComparisonOp::Eq,
858            Box::new(IntegerExpression::Const(procs_in_loc)),
859        ));
860    }
861    ta.replace_initial_location_constraints(location_constraints);
862
863    println!("Please input the initial variable valuations:");
864    let mut variable_constraints = Vec::new();
865    for var in ta.variables() {
866        let var_value = loop {
867            println!("{}:", var.name());
868            let mut num = String::new();
869            io::stdin().read_line(&mut num).unwrap();
870            match num.trim().parse::<u32>() {
871                Ok(num) => break num,
872                _ => println!("Invalid input. Please enter a number greater or equal to 0."),
873            }
874        };
875        variable_constraints.push(BooleanExpression::ComparisonExpression(
876            Box::new(IntegerExpression::Atom(Variable::new(var.name()))),
877            ComparisonOp::Eq,
878            Box::new(IntegerExpression::Const(var_value)),
879        ));
880    }
881    ta.replace_initial_variable_constraints(variable_constraints);
882}
883
884/// Translate a threshold automaton into the specified format and write the
885/// resulting specification to the specified output file
886pub(crate) fn translate_ta(
887    ta: &GeneralThresholdAutomaton,
888    spec: &ELTLSpecification,
889    out: TranslationOutput,
890) -> Result<(), anyhow::Error> {
891    let translated_str: String = match out.output_format {
892        SpecFileFormat::BYMC => into_bymc(ta, spec),
893        SpecFileFormat::TLA => into_tla(ta, spec),
894    };
895
896    fs::write(out.output_file, translated_str).with_context(|| "Failed to write output file")?;
897    info!("Finished writing output file");
898
899    Ok(())
900}