1
use std::ffi::OsStr;
2
use std::fs::File;
3
use std::path::PathBuf;
4
use std::process::ExitCode;
5

            
6
use clap::Parser;
7
use clap::Subcommand;
8

            
9
use itertools::Itertools;
10
use merc_io::LargeFormatter;
11
use merc_ldd::Storage;
12
use merc_ldd::len;
13
use merc_lts::AutStream;
14
use merc_lts::LtsBuilderMem;
15
use merc_lts::LtsFormat;
16
use merc_lts::guess_lts_format_from_extension;
17
use merc_lts::write_bcg;
18
use merc_symbolic::SymFormat;
19
use merc_symbolic::SymbolicLTS;
20
use merc_symbolic::SymbolicLtsBdd;
21
use merc_symbolic::convert_symbolic_lts;
22
use merc_symbolic::guess_format_from_extension;
23
use merc_symbolic::parse_compacted_dependency_graph;
24
use merc_symbolic::reachability;
25
use merc_symbolic::reachability_bdd;
26
use merc_symbolic::read_sylvan;
27
use merc_symbolic::read_symbolic_lts;
28
use merc_symbolic::refine_bisimulation;
29
use merc_symbolic::reorder;
30
use merc_symbolic::sigref_symbolic;
31
use merc_tools::VerbosityFlag;
32
use merc_tools::Version;
33
use merc_tools::VersionFlag;
34
use merc_unsafety::print_allocator_metrics;
35
use merc_utilities::MercError;
36
use merc_utilities::Timing;
37
use oxidd::BooleanFunction;
38
use oxidd::bdd::BDDFunction;
39
use oxidd::util::SatCountCache;
40
use rustc_hash::FxBuildHasher;
41
use which::which_in;
42

            
43
/// Default node capacity for the Oxidd decision diagram manager.
44
const DEFAULT_OXIDD_NODE_CAPACITY: usize = 2028;
45

            
46
/// A command line tool for symbolic labelled transition systems
47
#[derive(clap::Parser, Debug)]
48
#[command(arg_required_else_help = true)]
49
struct Cli {
50
    #[command(flatten)]
51
    version: VersionFlag,
52

            
53
    #[command(flatten)]
54
    verbosity: VerbosityFlag,
55

            
56
    #[command(subcommand)]
57
    commands: Option<Commands>,
58

            
59
    /// Number of workers for the Oxidd decision diagram manager.
60
    #[arg(long, global = true, default_value_t = 1)]
61
    oxidd_workers: u32,
62

            
63
    /// Node capacity for the Oxidd decision diagram manager.
64
    #[arg(long, global = true, default_value_t = DEFAULT_OXIDD_NODE_CAPACITY)]
65
    oxidd_node_capacity: usize,
66

            
67
    /// Cache capacity for the Oxidd decision diagram manager, if `None` it is set to the node capacity.
68
    #[arg(long, global = true)]
69
    oxidd_cache_capacity: Option<usize>,
70

            
71
    #[arg(long, global = true)]
72
    timings: bool,
73
}
74

            
75
/// Defines the subcommands for this tool.
76
#[derive(Debug, Subcommand)]
77
enum Commands {
78
    /// Print information related to the given symbolic LTS.
79
    Info(InfoArgs),
80
    /// Explore the given symbolic LTS.
81
    Explore(ExploreArgs),
82
    /// Computes a reordering for a dependency graph given by lpsreach or pbessolvesymbolic.
83
    Reorder(ReorderArgs),
84
    /// Convert a symbolic LTS to a concrete LTS.
85
    Convert(ConvertArgs),
86
    /// Apply reductions to a symbolic LTS.
87
    Reduce(ReduceArgs),
88
}
89

            
90
#[derive(clap::Args, Debug)]
91
struct InfoArgs {
92
    filename: PathBuf,
93

            
94
    /// Sets the input symbolic LTS format.
95
    #[arg(long)]
96
    format: Option<SymFormat>,
97
}
98

            
99
#[derive(clap::Args, Debug)]
100
struct ExploreArgs {
101
    filename: PathBuf,
102

            
103
    /// Sets the input symbolic LTS format.
104
    #[arg(long)]
105
    format: Option<SymFormat>,
106

            
107
    /// Use BDD based exploration by converting the symbolic LTS
108
    #[arg(long)]
109
    use_bdd: bool,
110

            
111
    /// Visualize intermediate BDDs using oxidd-vis.
112
    #[arg(long)]
113
    visualize: bool,
114
}
115

            
116
#[derive(clap::Args, Debug)]
117
struct ReorderArgs {
118
    /// Explicit path to the mCRL2 tools (lpsreach or pbessolvesymbolic)
119
    #[arg(long)]
120
    mcrl2_path: Option<PathBuf>,
121

            
122
    /// Explicit path to the kahypar tools
123
    #[arg(long)]
124
    kahypar_path: Option<PathBuf>,
125

            
126
    /// Explicit path to the kahypar.ini file to use.
127
    #[arg(long)]
128
    kahypar_ini_path: Option<PathBuf>,
129

            
130
    /// The input linear process specification file in the mCRL2 .lps format.
131
    filename: PathBuf,
132
}
133

            
134
#[derive(clap::Args, Debug)]
135
struct ConvertArgs {
136
    /// Sets the input symbolic LTS format.
137
    #[arg(long)]
138
    format: Option<SymFormat>,
139

            
140
    /// The input symbolic LTS file path.
141
    filename: PathBuf,
142

            
143
    /// Sets the output LTS format.
144
    #[arg(long)]
145
    output_format: Option<LtsFormat>,
146

            
147
    /// The output LTS file path.
148
    output: PathBuf,
149
}
150

            
151
#[derive(Clone, Copy, clap::ValueEnum, Debug)]
152
enum Equivalence {
153
    StrongBisim,
154
    StrongBisimSigref,
155
}
156

            
157
#[derive(clap::Args, Debug)]
158
struct ReduceArgs {
159
    /// The equivalence relation to reduce modulo.
160
    equivalence: Equivalence,
161

            
162
    /// The input symbolic LTS file path.
163
    filename: PathBuf,
164

            
165
    /// Sets the input symbolic LTS format.
166
    #[arg(long)]
167
    format: Option<SymFormat>,
168

            
169
    /// Sets the output LTS format.
170
    #[arg(long)]
171
    output_format: Option<LtsFormat>,
172

            
173
    /// Visualize the reduction steps in oxidd-vis.
174
    #[arg(long)]
175
    visualize: bool,
176

            
177
    /// Split the signature per transition group.
178
    #[arg(long)]
179
    split_signature: bool,
180

            
181
    /// Extend each transition relation to the full state and next-state domain.
182
    #[arg(long)]
183
    extend_relation: bool,
184

            
185
    /// Merge transition relations into a single relation before signature computation.
186
    #[arg(long)]
187
    merge_transitions: bool,
188
}
189

            
190
/// Initializes the Oxidd BDD manager based on CLI arguments.
191
fn init_bdd_manager(cli: &Cli) -> oxidd::bdd::BDDManagerRef {
192
    oxidd::bdd::new_manager(
193
        cli.oxidd_node_capacity,
194
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
195
        cli.oxidd_workers,
196
    )
197
}
198

            
199
fn main() -> Result<ExitCode, MercError> {
200
    let cli = Cli::parse();
201

            
202
    env_logger::Builder::new()
203
        .filter_level(cli.verbosity.log_level_filter())
204
        .parse_default_env()
205
        .init();
206

            
207
    if cli.version.into() {
208
        eprintln!("{}", Version);
209
        return Ok(ExitCode::SUCCESS);
210
    }
211

            
212
    let timing = Timing::new();
213

            
214
    if let Some(command) = &cli.commands {
215
        match command {
216
            Commands::Info(args) => handle_info(args, &timing)?,
217
            Commands::Explore(args) => handle_explore(&cli, args, &timing)?,
218
            Commands::Reorder(args) => handle_reorder(args, &timing)?,
219
            Commands::Convert(args) => handle_convert(args, &timing)?,
220
            Commands::Reduce(args) => handle_reduce(&cli, args, &timing)?,
221
        }
222
    }
223

            
224
    if cli.timings {
225
        timing.print();
226
    }
227

            
228
    print_allocator_metrics();
229
    Ok(ExitCode::SUCCESS)
230
}
231

            
232
/// Reads the given symbolic LTS and prints information about it.
233
fn handle_info(args: &InfoArgs, timing: &Timing) -> Result<(), MercError> {
234
    let mut storage = Storage::new();
235

            
236
    let format =
237
        guess_format_from_extension(&args.filename, args.format).ok_or("Cannot determine input symbolic LTS format")?;
238

            
239
    if format != SymFormat::Sym {
240
        return Err("Currently only the .sym format is supported for info".into());
241
    }
242

            
243
    let lts = timing.measure("read_symbolic_lts", || {
244
        read_symbolic_lts(&mut storage, File::open(&args.filename)?)
245
    })?;
246

            
247
    println!(
248
        "Number of states: {}",
249
        LargeFormatter(merc_ldd::len(&mut storage, lts.states()))
250
    );
251
    println!("Number of summand groups: {}", lts.transition_groups().len());
252

            
253
    Ok(())
254
}
255

            
256
/// Explores the given symbolic LTS.
257
fn handle_explore(cli: &Cli, args: &ExploreArgs, timing: &Timing) -> Result<(), MercError> {
258
    let mut storage = Storage::new();
259

            
260
    let format = guess_format_from_extension(&args.filename, args.format).ok_or("Cannot determine input format")?;
261

            
262
    let mut file = File::open(&args.filename)?;
263
    match format {
264
        SymFormat::Sylvan => {
265
            let mut lts = timing.measure("read_symbolic_lts", || read_sylvan(&mut storage, &mut file))?;
266
            explore_impl(&mut storage, cli, args, &mut lts, timing)?;
267
        }
268
        SymFormat::Sym => {
269
            let mut lts = timing.measure("read_symbolic_lts", || read_symbolic_lts(&mut storage, &mut file))?;
270
            explore_impl(&mut storage, cli, args, &mut lts, timing)?;
271
        }
272
    }
273

            
274
    Ok(())
275
}
276

            
277
fn explore_impl<L: SymbolicLTS>(
278
    storage: &mut Storage,
279
    cli: &Cli,
280
    args: &ExploreArgs,
281
    lts: &mut L,
282
    timing: &Timing,
283
) -> Result<(), MercError> {
284
    if args.use_bdd {
285
        let manager_ref = init_bdd_manager(cli);
286

            
287
        let lts_bdd = timing.measure("convert_bdd", || {
288
            SymbolicLtsBdd::from_symbolic_lts(storage, &manager_ref, lts)
289
        })?;
290

            
291
        println!(
292
            "LTS has {} states",
293
            timing.measure("explore_bdd", || -> Result<_, MercError> {
294
                let reachable_states_bdd = reachability_bdd(&manager_ref, &lts_bdd, args.visualize)?;
295

            
296
                let num_reachable_states_bdd = reachable_states_bdd.sat_count::<u64, FxBuildHasher>(
297
                    lts_bdd.state_variables().len() as u32,
298
                    &mut SatCountCache::default(),
299
                ) as usize;
300
                Ok(num_reachable_states_bdd)
301
            })?
302
        );
303
    } else {
304
        println!(
305
            "LTS has {} states",
306
            timing.measure("explore", || -> Result<_, MercError> {
307
                let states = reachability(storage, lts, timing)?;
308

            
309
                Ok(len(storage, &states))
310
            })?
311
        );
312
    }
313
    Ok(())
314
}
315

            
316
/// Computes a variable reordering for the output of lpsreach.
317
fn handle_reorder(args: &ReorderArgs, _timing: &Timing) -> Result<(), MercError> {
318
    // Find kahypar
319
    let kahypar_path = if let Some(path) = &args.kahypar_path {
320
        which_in("KaHyPar", Some(path), std::env::current_dir()?).map_err(|_e| "Cannot find KaHyPar")?
321
    } else {
322
        which::which("KaHyPar").map_err(|_e| "Cannot find KaHyPar in PATH")?
323
    };
324

            
325
    let kahypar_ini_path = if let Some(path) = &args.kahypar_ini_path {
326
        if !path.is_file() {
327
            return Err(format!(
328
                "The specified kahypar.ini path '{}' does not exist or is not a file.",
329
                path.display()
330
            )
331
            .into());
332
        }
333
        path.clone()
334
    } else {
335
        // Get path relative to the current executable, and obtain a path to the `kahypar.ini` configuration file.
336
        let mut default_kahypar_ini_path = std::env::current_exe()?;
337
        default_kahypar_ini_path.pop(); // remove the executable filename
338
        default_kahypar_ini_path.push("kahypar.ini");
339

            
340
        if !default_kahypar_ini_path.is_file() {
341
            return Err(format!(
342
                "Could not find '{}'. The 'kahypar.ini' file must be present next to the executable, or passed via --kahypar-ini-path.",
343
                default_kahypar_ini_path.display()
344
            )
345
            .into());
346
        }
347
        default_kahypar_ini_path
348
    };
349

            
350
    if args.filename.extension() == Some(OsStr::new("lps")) {
351
        // Find lpsreach
352
        let lpsreach_path = if let Some(path) = &args.mcrl2_path {
353
            which_in("lpsreach", Some(path), std::env::current_dir()?).map_err(|_e| "Cannot find lpsreach")?
354
        } else {
355
            which::which("lpsreach").map_err(|_e| "Cannot find lpsreach in PATH")?
356
        };
357

            
358
        // Run lpsreach with the --info flag to get dependency information
359
        let proc = duct::cmd!(lpsreach_path, "--info", &args.filename)
360
            .stdout_capture()
361
            .run()
362
            .map_err(|e| e.to_string())?;
363

            
364
        let graph = parse_compacted_dependency_graph(str::from_utf8(&proc.stdout)?);
365

            
366
        let order = reorder(&kahypar_path, &kahypar_ini_path, &graph)?;
367
        println!("Computed variable order: {}", order.iter().format(" "));
368
    } else if args.filename.extension() == Some(OsStr::new("pbes")) {
369
        // Find pbessolvesymbolic
370
        let pbessolvesymbolic = if let Some(path) = &args.mcrl2_path {
371
            which_in("pbessolvesymbolic", Some(path), std::env::current_dir()?)
372
                .map_err(|_e| "Cannot find pbessolvesymbolic")?
373
        } else {
374
            which::which("pbessolvesymbolic").map_err(|_e| "Cannot find pbessolvesymbolic in PATH")?
375
        };
376

            
377
        // Run pbessolvesymbolic with the --info flag to get dependency information
378
        let proc = duct::cmd!(pbessolvesymbolic, "--info", &args.filename)
379
            .stdout_capture()
380
            .run()
381
            .map_err(|e| e.to_string())?;
382

            
383
        let graph = parse_compacted_dependency_graph(str::from_utf8(&proc.stdout)?);
384
        let order = reorder(&kahypar_path, &kahypar_ini_path, &graph)?;
385

            
386
        // Ensure that the first variable is 0 by removing it from order and printing it explicitly
387
        println!(
388
            "Computed variable order: 0 {}",
389
            order.iter().filter(|&x| *x != 0).format(" ")
390
        );
391
    } else {
392
        return Err("Input file must be either a .lps or .pbes file".into());
393
    }
394

            
395
    Ok(())
396
}
397

            
398
/// Converts a symbolic LTS to an explicit LTS.
399
fn handle_convert(args: &ConvertArgs, _timing: &Timing) -> Result<(), MercError> {
400
    let mut storage = Storage::new();
401

            
402
    let format =
403
        guess_format_from_extension(&args.filename, args.format).ok_or("Cannot determine input symbolic LTS format")?;
404
    if format != SymFormat::Sym {
405
        return Err("Currently only the .sym format is supported for conversion".into());
406
    }
407

            
408
    let mut file = File::open(&args.filename)?;
409
    let lts = read_symbolic_lts(&mut storage, &mut file)?;
410

            
411
    let output_format = guess_lts_format_from_extension(&args.output, args.output_format)
412
        .ok_or("Cannot determine output LTS format")?;
413

            
414
    match output_format {
415
        LtsFormat::Lts => {
416
            unimplemented!("Writing LTS format is not yet implemented");
417
        }
418
        LtsFormat::Aut | LtsFormat::AutMcrl2 => {
419
            let mut output = File::create(&args.output)?;
420
            let mut stream = AutStream::new(&mut output);
421
            convert_symbolic_lts(&mut storage, &mut stream, &lts)?;
422
        }
423
        LtsFormat::Bcg => {
424
            let explicit_lts =
425
                convert_symbolic_lts(&mut storage, &mut LtsBuilderMem::new(Vec::new(), Vec::new()), &lts)?;
426
            write_bcg(&explicit_lts, &args.output)?;
427
        }
428
    }
429

            
430
    Ok(())
431
}
432

            
433
/// Applies reductions to a symbolic LTS.
434
fn handle_reduce(cli: &Cli, args: &ReduceArgs, timing: &Timing) -> Result<(), MercError> {
435
    let format =
436
        guess_format_from_extension(&args.filename, args.format).ok_or("Cannot determine input symbolic LTS format")?;
437
    if format != SymFormat::Sym {
438
        return Err("Currently only the .sym format is supported for conversion".into());
439
    }
440

            
441
    let mut storage = Storage::new();
442
    let manager_ref = init_bdd_manager(cli);
443

            
444
    let mut file = File::open(&args.filename)?;
445
    let lts = read_symbolic_lts(&mut storage, &mut file)?;
446

            
447
    let lts_bdd = timing.measure("convert_bdd", || {
448
        SymbolicLtsBdd::from_symbolic_lts(&mut storage, &manager_ref, &lts)
449
    })?;
450

            
451
    let _quotient = timing.measure("reduction", || -> Result<BDDFunction, MercError> {
452
        match args.equivalence {
453
            Equivalence::StrongBisimSigref => sigref_symbolic(
454
                &manager_ref,
455
                &lts_bdd,
456
                timing,
457
                args.split_signature,
458
                args.extend_relation,
459
                args.merge_transitions,
460
                args.visualize,
461
            )
462
            .map(|(quotient, _, _)| quotient),
463
            Equivalence::StrongBisim => refine_bisimulation(&manager_ref, &lts_bdd),
464
        }
465
    })?;
466

            
467
    Ok(())
468
}