1
use std::fs::File;
2
use std::fs::read_to_string;
3
use std::io::Write;
4
use std::path::Path;
5
use std::path::PathBuf;
6
use std::process::ExitCode;
7

            
8
use clap::Parser;
9
use clap::Subcommand;
10
use duct::cmd;
11
use itertools::Itertools;
12
use log::debug;
13
use log::info;
14
use merc_lts::LtsFormat;
15
use merc_lts::guess_lts_format_from_extension;
16
use merc_lts::read_aut;
17
use merc_lts::write_aut;
18
use merc_symbolic::bits_to_bdd;
19
use merc_vpg::Projected;
20
use merc_vpg::ProjectedLts;
21
use merc_vpg::Solver;
22
use merc_vpg::project_feature_transition_system_iter;
23
use merc_vpg::solve_priority_promotion;
24
use merc_vpg::verify_solution;
25
use oxidd::BooleanFunction;
26

            
27
use merc_symbolic::CubeIterAll;
28
use merc_symbolic::FormatConfig;
29
use merc_syntax::UntypedStateFrmSpec;
30
use merc_tools::VerbosityFlag;
31
use merc_tools::Version;
32
use merc_tools::VersionFlag;
33
use merc_tools::format_key_values_json;
34
use merc_tools::report_error;
35
use merc_unsafety::print_allocator_metrics;
36
use merc_utilities::MercError;
37
use merc_utilities::Timing;
38
use merc_vpg::FeatureDiagram;
39
use merc_vpg::ParityGameFormat;
40
use merc_vpg::PgDot;
41
use merc_vpg::Player;
42
use merc_vpg::VpgDot;
43
use merc_vpg::VpgSolver;
44
use merc_vpg::compute_reachable;
45
use merc_vpg::guess_format_from_extension;
46
use merc_vpg::make_vpg_total;
47
use merc_vpg::project_variability_parity_games_iter;
48
use merc_vpg::read_fts;
49
use merc_vpg::read_pg;
50
use merc_vpg::read_vpg;
51
use merc_vpg::solve_variability_product_zielonka;
52
use merc_vpg::solve_variability_zielonka;
53
use merc_vpg::solve_zielonka;
54
use merc_vpg::translate;
55
use merc_vpg::translate_vpg;
56
use merc_vpg::verify_variability_product_zielonka_solution;
57
use merc_vpg::write_pg;
58
use merc_vpg::write_vpg;
59

            
60
/// Default node capacity for the Oxidd decision diagram manager. The choice
61
/// for this value is fairly arbitrary.
62
const DEFAULT_OXIDD_NODE_CAPACITY: usize = 2028;
63

            
64
/// A command line tool for variability parity games
65
#[derive(clap::Parser, Debug)]
66
#[command(arg_required_else_help = true)]
67
struct Cli {
68
    #[command(flatten)]
69
    version: VersionFlag,
70

            
71
    #[command(flatten)]
72
    verbosity: VerbosityFlag,
73

            
74
    #[arg(long, global = true)]
75
    timings: bool,
76

            
77
    #[arg(long, global = true, default_value_t = 1)]
78
    oxidd_workers: u32,
79

            
80
    #[arg(long, global = true, default_value_t = DEFAULT_OXIDD_NODE_CAPACITY)]
81
    oxidd_node_capacity: usize,
82

            
83
    #[arg(long, global = true)]
84
    oxidd_cache_capacity: Option<usize>,
85

            
86
    #[command(subcommand)]
87
    commands: Option<Commands>,
88
}
89

            
90
#[derive(Debug, Subcommand)]
91
enum Commands {
92
    Solve(SolveArgs),
93
    Reachable(ReachableArgs),
94
    Project(ProjectArgs),
95
    ProjectVpg(ProjectVpgArgs),
96
    Translate(TranslateArgs),
97
    TranslateVpg(TranslateVpgArgs),
98
    Display(DisplayArgs),
99
}
100

            
101
/// Solve a parity game
102
#[derive(clap::Args, Debug)]
103
struct SolveArgs {
104
    filename: PathBuf,
105

            
106
    /// The input parity game file format
107
    #[arg(long)]
108
    format: Option<ParityGameFormat>,
109

            
110
    /// Sets the solving variant for regular parity games.
111
    #[arg(long, default_value_t = Solver::Zielonka)]
112
    solver: Solver,
113

            
114
    /// Sets the solving variant used for variability parity games.
115
    #[arg(long, default_value_t = VpgSolver::Family)]
116
    vpg_solver: VpgSolver,
117

            
118
    /// Output the solution for every single vertex instead of only the initial vertex.
119
    #[arg(long, default_value_t = false)]
120
    full_solution: bool,
121

            
122
    /// Whether to verify the solution after computing it
123
    #[arg(long, default_value_t = false)]
124
    verify_solution: bool,
125
}
126

            
127
/// Compute the reachable part of a parity game
128
#[derive(clap::Args, Debug)]
129
struct ReachableArgs {
130
    /// The filename of the parity game to compute the reachable part of
131
    filename: PathBuf,
132

            
133
    /// The output filename for the reachable part of the parity game
134
    output: PathBuf,
135

            
136
    #[arg(long, short)]
137
    format: Option<ParityGameFormat>,
138
}
139

            
140
/// Project a feature transition system to a set of transition systems
141
#[derive(clap::Args, Debug)]
142
struct ProjectArgs {
143
    /// The filename of the variability parity game to project
144
    filename: PathBuf,
145

            
146
    /// The filename of the feature diagram
147
    feature_diagram_filename: PathBuf,
148

            
149
    /// The output filename pattern for the projected labelled transition systems.
150
    output: String,
151

            
152
    /// The input featured transition system file format
153
    #[arg(long, short)]
154
    format: Option<LtsFormat>,
155
}
156

            
157
/// Project a variability parity game to a set of parity games
158
#[derive(clap::Args, Debug)]
159
struct ProjectVpgArgs {
160
    /// The filename of the variability parity game to project
161
    filename: PathBuf,
162

            
163
    /// The output filename pattern for the projected parity games.
164
    output: String,
165

            
166
    /// Whether to compute the reachable part of each projection.
167
    #[arg(long, short)]
168
    reachable: bool,
169

            
170
    #[arg(long, short)]
171
    format: Option<ParityGameFormat>,
172
}
173

            
174
/// Translate a labelled transition system and a modal formula into a parity game
175
#[derive(clap::Args, Debug)]
176
struct TranslateArgs {
177
    /// The filename of the labelled transition system
178
    labelled_transition_system: PathBuf,
179

            
180
    /// The input labelled transition system file format
181
    #[arg(long, short)]
182
    format: Option<LtsFormat>,
183

            
184
    /// The filename of the modal formula
185
    formula_filename: PathBuf,
186

            
187
    /// The parity game output filename
188
    output: PathBuf,
189
}
190

            
191
/// Translate a feature transition system and a modal formula into a variability parity game
192
#[derive(clap::Args, Debug)]
193
struct TranslateVpgArgs {
194
    /// The filename of the feature diagram
195
    feature_diagram_filename: PathBuf,
196

            
197
    /// The input featured transition system file format
198
    #[arg(long, short)]
199
    format: Option<LtsFormat>,
200

            
201
    /// The filename of the feature transition system
202
    fts_filename: PathBuf,
203

            
204
    /// The filename of the modal formula
205
    formula_filename: PathBuf,
206

            
207
    /// The variability parity game output filename
208
    output: String,
209
}
210

            
211
/// Display a (variability) parity game
212
#[derive(clap::Args, Debug)]
213
struct DisplayArgs {
214
    /// The filename of the (variability) parity game to display
215
    filename: PathBuf,
216

            
217
    /// The .dot file output filename
218
    output: PathBuf,
219

            
220
    /// The parity game file format
221
    #[arg(long, short)]
222
    format: Option<ParityGameFormat>,
223
}
224

            
225
fn main() -> ExitCode {
226
    let cli = Cli::parse();
227

            
228
    let mut timing = Timing::new();
229

            
230
    env_logger::Builder::new()
231
        .filter_level(cli.verbosity.log_level_filter())
232
        .format_key_values(|formatter, source| format_key_values_json(formatter, source))
233
        .parse_default_env()
234
        .init();
235

            
236
    if cli.version.into() {
237
        eprintln!("{}", Version);
238
        return ExitCode::SUCCESS;
239
    }
240

            
241
    let result = handle_command(&cli, &mut timing);
242

            
243
    if cli.timings {
244
        timing.print();
245
    }
246

            
247
    print_allocator_metrics();
248
    if cfg!(feature = "merc_metrics") {
249
        oxidd::bdd::print_stats();
250
    }
251
    report_error(result)
252
}
253

            
254
fn handle_command(cli: &Cli, timing: &mut Timing) -> Result<(), MercError> {
255
    if let Some(command) = &cli.commands {
256
        match command {
257
            Commands::Solve(args) => handle_solve(cli, args, timing)?,
258
            Commands::Reachable(args) => handle_reachable(cli, args, timing)?,
259
            Commands::Project(args) => handle_project_fts(cli, args, timing)?,
260
            Commands::ProjectVpg(args) => handle_project_vpg(cli, args, timing)?,
261
            Commands::Translate(args) => handle_translate(args)?,
262
            Commands::TranslateVpg(args) => handle_translate_vpg(cli, args)?,
263
            Commands::Display(args) => handle_display(cli, args, timing)?,
264
        }
265
    }
266

            
267
    Ok(())
268
}
269

            
270
/// Handle the `solve` subcommand.
271
///
272
/// Reads either a standard parity game (PG) or a variability parity game (VPG)
273
/// based on the provided format or filename extension, then solves it using
274
/// Zielonka's algorithm.
275
fn handle_solve(cli: &Cli, args: &SolveArgs, timing: &mut Timing) -> Result<(), MercError> {
276
    let path = Path::new(&args.filename);
277
    let mut file = File::open(path)?;
278
    let format = guess_format_from_extension(path, args.format)
279
        .ok_or_else(|| format!("Unknown parity game file format for '{}", path.display()))?;
280

            
281
    if format == ParityGameFormat::PG {
282
        // Read and solve a standard parity game.
283
        let game = timing.measure("read_pg", || read_pg(&mut file))?;
284

            
285
        let (solution, strategy) = timing.measure("solve_zielonka", || match args.solver {
286
            Solver::Zielonka => solve_zielonka(&game, args.verify_solution),
287
            Solver::PriorityPromotion => solve_priority_promotion(&game, args.verify_solution),
288
        });
289
        if args.full_solution {
290
            for (index, player_set) in solution.iter().enumerate() {
291
                println!("W{index}: {}", player_set.iter_ones().format(", "));
292
            }
293
        } else if solution[0][0] {
294
            println!("{}", Player::Even.solution())
295
        } else {
296
            println!("{}", Player::Odd.solution())
297
        }
298

            
299
        if let Some(strategy) = strategy
300
            && args.verify_solution
301
        {
302
            verify_solution(&game, &solution, &strategy);
303
        }
304
    } else {
305
        // Read and solve a variability parity game.
306
        let manager_ref = oxidd::bdd::new_manager(
307
            cli.oxidd_node_capacity,
308
            cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
309
            cli.oxidd_workers,
310
        );
311

            
312
        let game = timing.measure("read_vpg", || -> Result<_, MercError> {
313
            read_vpg(&manager_ref, &mut file)
314
        })?;
315

            
316
        let game = if !game.is_vpg_total(&manager_ref)? {
317
            info!("Making the VPG total...");
318
            make_vpg_total(&manager_ref, &game)?
319
        } else {
320
            game
321
        };
322

            
323
        timing.measure("solve_variability_zielonka", || -> Result<_, MercError> {
324
            if args.vpg_solver == VpgSolver::Product {
325
                let solver = args.solver;
326

            
327
                // Since we want to print W0, W1 separately, we need to store the results temporarily.
328
                let mut results = [Vec::new(), Vec::new()];
329
                for result in solve_variability_product_zielonka(&game, solver, timing) {
330
                    let (cube, _bdd, solution) = result?;
331

            
332
                    for (index, w) in solution.iter().enumerate() {
333
                        results[index].push((cube.clone(), w.clone()));
334
                    }
335
                }
336

            
337
                for (index, w) in results.iter().enumerate() {
338
                    for (cube, vertices) in w {
339
                        println!(
340
                            "W{index}: For product {} the following vertices are in: {}",
341
                            FormatConfig(cube),
342
                            vertices
343
                                .iter_ones()
344
                                .filter(|v| if args.full_solution { true } else { *v == 0 })
345
                                .format(", ")
346
                        );
347
                    }
348
                }
349
            } else {
350
                let solutions = solve_variability_zielonka(&manager_ref, &game, args.vpg_solver, false)?;
351
                for (index, w) in solutions.iter().enumerate() {
352
                    for entry in CubeIterAll::new(game.configuration()) {
353
                        let config = entry?;
354
                        let config_function = bits_to_bdd(&manager_ref, game.variables(), &config)?;
355

            
356
                        println!(
357
                            "W{index}: For product {} the following vertices are in: {}",
358
                            FormatConfig(&config),
359
                            w.iter() // Do not use iter_vertices because the first one is the initial vertex only
360
                                .take(if args.full_solution { usize::MAX } else { 1 }) // Take only first if we don't want full solution
361
                                .filter(|(_v, config)| { config.and(&config_function).unwrap().satisfiable() })
362
                                .map(|(v, _)| v)
363
                                .format(", ")
364
                        );
365
                    }
366
                }
367

            
368
                if args.verify_solution {
369
                    verify_variability_product_zielonka_solution(&game, &solutions, timing)?;
370
                }
371
            }
372

            
373
            Ok(())
374
        })?;
375
    }
376

            
377
    Ok(())
378
}
379

            
380
/// Handle the `reachable` subcommand.
381
///
382
/// Reads a PG or VPG, computes its reachable part, and writes it to `output`.
383
/// Also logs the vertex index mapping to aid inspection.
384
fn handle_reachable(cli: &Cli, args: &ReachableArgs, timing: &mut Timing) -> Result<(), MercError> {
385
    let path = Path::new(&args.filename);
386
    let format = guess_format_from_extension(path, args.format)
387
        .ok_or_else(|| format!("Unknown parity game file format for '{}", path.display()))?;
388

            
389
    let mut file = File::open(path)?;
390
    match format {
391
        ParityGameFormat::PG => {
392
            let game = timing.measure("read_pg", || read_pg(&mut file))?;
393

            
394
            let (reachable_game, mapping) = timing.measure("compute_reachable", || compute_reachable(&game));
395

            
396
            for (old_index, new_index) in mapping.iter().enumerate() {
397
                debug!("{} -> {:?}", old_index, new_index);
398
            }
399

            
400
            let mut output_file = File::create(&args.output)?;
401
            write_pg(&mut output_file, &reachable_game)?;
402
        }
403
        ParityGameFormat::VPG => {
404
            let manager_ref = oxidd::bdd::new_manager(
405
                cli.oxidd_node_capacity,
406
                cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
407
                cli.oxidd_workers,
408
            );
409

            
410
            let game = timing.measure("read_vpg", || read_vpg(&manager_ref, &mut file))?;
411
            let (reachable_game, mapping) = timing.measure("compute_reachable_vpg", || compute_reachable(&game));
412

            
413
            for (old_index, new_index) in mapping.iter().enumerate() {
414
                debug!("{} -> {:?}", old_index, new_index);
415
            }
416

            
417
            let mut output_file = File::create(&args.output)?;
418
            // Write reachable part using the PG writer, as reachable_game is a ParityGame.
419
            write_pg(&mut output_file, &reachable_game)?;
420
        }
421
    }
422

            
423
    Ok(())
424
}
425

            
426
/// Projects a feature transition system to a set of transition systems and writes them to output.
427
fn handle_project_fts(cli: &Cli, args: &ProjectArgs, timing: &mut Timing) -> Result<(), MercError> {
428
    let format = guess_lts_format_from_extension(&args.filename, args.format).ok_or_else(|| {
429
        format!(
430
            "Unknown featured transition system format for '{}'.",
431
            args.filename.display()
432
        )
433
    })?;
434

            
435
    if format != LtsFormat::Aut {
436
        return Err(MercError::from(
437
            "The project command only works for featured transition systems in the .aut format.",
438
        ));
439
    }
440

            
441
    // Read and solve a variability parity game.
442
    let manager_ref = oxidd::bdd::new_manager(
443
        cli.oxidd_node_capacity,
444
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
445
        cli.oxidd_workers,
446
    );
447

            
448
    // Read feature diagram
449
    let mut feature_diagram_file = File::open(&args.feature_diagram_filename).map_err(|e| {
450
        MercError::from(format!(
451
            "Could not open feature diagram file '{}': {}",
452
            &args.feature_diagram_filename.display(),
453
            e
454
        ))
455
    })?;
456
    let feature_diagram = FeatureDiagram::from_reader(&manager_ref, &mut feature_diagram_file)?;
457
    debug!("Feature diagram {:?}", feature_diagram);
458

            
459
    // Read the feature transition system.
460
    let mut fts_file = File::open(&args.filename)?;
461
    let fts = read_fts(&manager_ref, &mut fts_file, feature_diagram.features().clone())?;
462
    let output_path = Path::new(&args.output);
463

            
464
    for result in project_feature_transition_system_iter(&fts, &feature_diagram, timing) {
465
        let (ProjectedLts { bits, bdd: _, lts }, _) = result?;
466

            
467
        let extension = output_path.extension().ok_or("Missing extension on output file")?;
468
        let new_path = output_path
469
            .with_file_name(format!(
470
                "{}_{}",
471
                output_path
472
                    .file_stem()
473
                    .ok_or("Missing filename on output")?
474
                    .to_string_lossy(),
475
                FormatConfig(&bits)
476
            ))
477
            .with_added_extension(extension);
478
        let mut output_file = File::create(new_path)?;
479

            
480
        write_aut(&mut output_file, &lts)?;
481
    }
482

            
483
    Ok(())
484
}
485

            
486
/// Compute all the projections of a variability parity game and write them to output.
487
fn handle_project_vpg(cli: &Cli, args: &ProjectVpgArgs, timing: &mut Timing) -> Result<(), MercError> {
488
    let format = guess_format_from_extension(&args.filename, args.format)
489
        .ok_or_else(|| format!("Unknown parity game file format for '{}'.", args.filename.display()))?;
490

            
491
    let mut file = File::open(&args.filename)?;
492
    if format != ParityGameFormat::VPG {
493
        return Err(MercError::from(
494
            "The project command only works for variability parity games.",
495
        ));
496
    }
497

            
498
    // Read the variability parity game.
499
    let manager_ref = oxidd::bdd::new_manager(
500
        cli.oxidd_node_capacity,
501
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
502
        cli.oxidd_workers,
503
    );
504

            
505
    let vpg = timing.measure("read_vpg", || read_vpg(&manager_ref, &mut file))?;
506
    let output_path = Path::new(&args.output);
507

            
508
    for result in project_variability_parity_games_iter(&vpg, timing) {
509
        let (Projected { bits, bdd: _, game }, _) = result?;
510

            
511
        let extension = output_path.extension().ok_or("Missing extension on output file")?;
512
        let new_path = output_path
513
            .with_file_name(format!(
514
                "{}_{}",
515
                output_path
516
                    .file_stem()
517
                    .ok_or("Missing filename on output")?
518
                    .to_string_lossy(),
519
                FormatConfig(&bits)
520
            ))
521
            .with_added_extension(extension);
522

            
523
        let mut output_file = File::create(new_path)?;
524

            
525
        if args.reachable {
526
            let (reachable_pg, _projection) = compute_reachable(&game);
527
            write_pg(&mut output_file, &reachable_pg)?;
528
        } else {
529
            write_pg(&mut output_file, &game)?;
530
        }
531
    }
532

            
533
    Ok(())
534
}
535

            
536
/// Handle the `translate` subcommand.
537
///
538
/// Translates a feature diagram, a feature transition system (FTS), and a modal
539
/// formula into a variability parity game.
540
fn handle_translate(args: &TranslateArgs) -> Result<(), MercError> {
541
    let format = guess_lts_format_from_extension(&args.labelled_transition_system, args.format).ok_or_else(|| {
542
        format!(
543
            "Unknown labelled transition system format for '{}'.",
544
            args.labelled_transition_system.display()
545
        )
546
    })?;
547

            
548
    if format != LtsFormat::Aut {
549
        return Err(MercError::from(
550
            "The translate command only works for labelled transition systems in the .aut format.",
551
        ));
552
    }
553

            
554
    // Read LTS
555
    let mut lts_file = File::open(&args.labelled_transition_system).map_err(|e| {
556
        MercError::from(format!(
557
            "Could not open feature transition system file '{}': {}",
558
            &args.labelled_transition_system.display(),
559
            e
560
        ))
561
    })?;
562
    let lts = read_aut(&mut lts_file)?;
563

            
564
    // Read and validate formula (no actions/data specs supported here)
565
    let formula_spec = UntypedStateFrmSpec::parse(&read_to_string(&args.formula_filename).map_err(|e| {
566
        MercError::from(format!(
567
            "Could not open formula file '{}': {}",
568
            &args.formula_filename.display(),
569
            e
570
        ))
571
    })?)?;
572

            
573
    if !formula_spec.action_declarations.is_empty() {
574
        return Err(MercError::from("We do not support formulas with action declarations."));
575
    }
576

            
577
    if !formula_spec.data_specification.is_empty() {
578
        return Err(MercError::from("The formula must not contain a data specification."));
579
    }
580

            
581
    let vpg = translate(&lts, &formula_spec.formula)?;
582

            
583
    let mut output_file = File::create(&args.output)?;
584
    write_pg(&mut output_file, &vpg)?;
585

            
586
    Ok(())
587
}
588

            
589
/// Handle the `translate_vpg` subcommand.
590
///
591
/// Translates a feature diagram, a feature transition system (FTS), and a modal
592
/// formula into a variability parity game.
593
fn handle_translate_vpg(cli: &Cli, args: &TranslateVpgArgs) -> Result<(), MercError> {
594
    let manager_ref = oxidd::bdd::new_manager(
595
        cli.oxidd_node_capacity,
596
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
597
        cli.oxidd_workers,
598
    );
599

            
600
    // Read feature diagram
601
    let mut feature_diagram_file = File::open(&args.feature_diagram_filename).map_err(|e| {
602
        MercError::from(format!(
603
            "Could not open feature diagram file '{}': {}",
604
            &args.feature_diagram_filename.display(),
605
            e
606
        ))
607
    })?;
608
    let feature_diagram = FeatureDiagram::from_reader(&manager_ref, &mut feature_diagram_file)?;
609

            
610
    // Read FTS
611
    let mut fts_file = File::open(&args.fts_filename).map_err(|e| {
612
        MercError::from(format!(
613
            "Could not open feature transition system file '{}': {}",
614
            &args.fts_filename.display(),
615
            e
616
        ))
617
    })?;
618
    let fts = read_fts(&manager_ref, &mut fts_file, feature_diagram.features().clone())?;
619

            
620
    // Read and validate formula (no actions/data specs supported here)
621
    let formula_spec = UntypedStateFrmSpec::parse(&read_to_string(&args.formula_filename).map_err(|e| {
622
        MercError::from(format!(
623
            "Could not open formula file '{}': {}",
624
            &args.formula_filename.display(),
625
            e
626
        ))
627
    })?)?;
628
    if !formula_spec.action_declarations.is_empty() {
629
        return Err(MercError::from("We do not support formulas with action declarations."));
630
    }
631

            
632
    if !formula_spec.data_specification.is_empty() {
633
        return Err(MercError::from("The formula must not contain a data specification."));
634
    }
635

            
636
    let vpg = translate_vpg(
637
        &manager_ref,
638
        &fts,
639
        feature_diagram.configuration().clone(),
640
        &formula_spec.formula,
641
    )?;
642
    let mut output_file = File::create(&args.output)?;
643
    write_vpg(&mut output_file, &vpg)?;
644

            
645
    Ok(())
646
}
647

            
648
/// Handle the `display` subcommand.
649
///
650
/// Reads a PG or VPG and writes a Graphviz `.dot` representation to `output`.
651
/// If the `dot` tool is available, also generates a PDF (`output.pdf`).
652
fn handle_display(cli: &Cli, args: &DisplayArgs, timing: &mut Timing) -> Result<(), MercError> {
653
    let path = Path::new(&args.filename);
654
    let mut file = File::open(path)?;
655
    let format = guess_format_from_extension(path, args.format)
656
        .ok_or_else(|| format!("Unknown parity game file format for '{}'.", path.display()))?;
657

            
658
    if format == ParityGameFormat::PG {
659
        // Read and display a standard parity game.
660
        let game = timing.measure("read_pg", || read_pg(&mut file))?;
661

            
662
        let mut output_file = File::create(&args.output)?;
663
        write!(&mut output_file, "{}", PgDot::new(&game))?;
664
    } else {
665
        // Read and display a variability parity game.
666
        let manager_ref = oxidd::bdd::new_manager(
667
            cli.oxidd_node_capacity,
668
            cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
669
            cli.oxidd_workers,
670
        );
671

            
672
        let game = timing.measure("read_vpg", || read_vpg(&manager_ref, &mut file))?;
673

            
674
        let mut output_file = File::create(&args.output)?;
675
        write!(&mut output_file, "{}", VpgDot::new(&game))?;
676
    }
677

            
678
    if let Ok(dot_path) = which::which("dot") {
679
        info!("Generating PDF using dot...");
680
        cmd!(dot_path, "-Tpdf", &args.output, "-O").run()?;
681
    }
682

            
683
    Ok(())
684
}