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_unsafety::print_allocator_metrics;
35
use merc_utilities::MercError;
36
use merc_utilities::Timing;
37
use merc_vpg::FeatureDiagram;
38
use merc_vpg::ParityGameFormat;
39
use merc_vpg::PgDot;
40
use merc_vpg::Player;
41
use merc_vpg::VpgDot;
42
use merc_vpg::VpgSolver;
43
use merc_vpg::compute_reachable;
44
use merc_vpg::guess_format_from_extension;
45
use merc_vpg::make_vpg_total;
46
use merc_vpg::project_variability_parity_games_iter;
47
use merc_vpg::read_fts;
48
use merc_vpg::read_pg;
49
use merc_vpg::read_vpg;
50
use merc_vpg::solve_variability_product_zielonka;
51
use merc_vpg::solve_variability_zielonka;
52
use merc_vpg::solve_zielonka;
53
use merc_vpg::translate;
54
use merc_vpg::translate_vpg;
55
use merc_vpg::verify_variability_product_zielonka_solution;
56
use merc_vpg::write_pg;
57
use merc_vpg::write_vpg;
58

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
224
fn main() -> Result<ExitCode, MercError> {
225
    let cli = Cli::parse();
226

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

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

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

            
240
    if let Some(command) = &cli.commands {
241
        match command {
242
            Commands::Solve(args) => handle_solve(&cli, args, &mut timing)?,
243
            Commands::Reachable(args) => handle_reachable(&cli, args, &mut timing)?,
244
            Commands::Project(args) => handle_project_fts(&cli, args, &mut timing)?,
245
            Commands::ProjectVpg(args) => handle_project_vpg(&cli, args, &mut timing)?,
246
            Commands::Translate(args) => handle_translate(args)?,
247
            Commands::TranslateVpg(args) => handle_translate_vpg(&cli, args)?,
248
            Commands::Display(args) => handle_display(&cli, args, &mut timing)?,
249
        }
250
    }
251

            
252
    if cli.timings {
253
        timing.print();
254
    }
255

            
256
    print_allocator_metrics();
257
    if cfg!(feature = "merc_metrics") {
258
        oxidd::bdd::print_stats();
259
    }
260
    Ok(ExitCode::SUCCESS)
261
}
262

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

            
274
    if format == ParityGameFormat::PG {
275
        // Read and solve a standard parity game.
276
        let game = timing.measure("read_pg", || read_pg(&mut file))?;
277

            
278
        let (solution, strategy) = timing.measure("solve_zielonka", || match args.solver {
279
            Solver::Zielonka => solve_zielonka(&game, args.verify_solution),
280
            Solver::PriorityPromotion => solve_priority_promotion(&game, args.verify_solution),
281
        });
282
        if args.full_solution {
283
            for (index, player_set) in solution.iter().enumerate() {
284
                println!("W{index}: {}", player_set.iter_ones().format(", "));
285
            }
286
        } else if solution[0][0] {
287
            println!("{}", Player::Even.solution())
288
        } else {
289
            println!("{}", Player::Odd.solution())
290
        }
291

            
292
        if let Some(strategy) = strategy
293
            && args.verify_solution
294
        {
295
            verify_solution(&game, &solution, &strategy);
296
        }
297
    } else {
298
        // Read and solve a variability parity game.
299
        let manager_ref = oxidd::bdd::new_manager(
300
            cli.oxidd_node_capacity,
301
            cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
302
            cli.oxidd_workers,
303
        );
304

            
305
        let game = timing.measure("read_vpg", || -> Result<_, MercError> {
306
            read_vpg(&manager_ref, &mut file)
307
        })?;
308

            
309
        let game = if !game.is_total(&manager_ref)? {
310
            info!("Making the VPG total...");
311
            make_vpg_total(&manager_ref, &game)?
312
        } else {
313
            game
314
        };
315

            
316
        timing.measure("solve_variability_zielonka", || -> Result<_, MercError> {
317
            if args.vpg_solver == VpgSolver::Product {
318
                let solver = args.solver;
319

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

            
325
                    for (index, w) in solution.iter().enumerate() {
326
                        results[index].push((cube.clone(), w.clone()));
327
                    }
328
                }
329

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

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

            
361
                if args.verify_solution {
362
                    verify_variability_product_zielonka_solution(&game, &solutions, timing)?;
363
                }
364
            }
365

            
366
            Ok(())
367
        })?;
368
    }
369

            
370
    Ok(())
371
}
372

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

            
382
    let mut file = File::open(path)?;
383
    match format {
384
        ParityGameFormat::PG => {
385
            let game = timing.measure("read_pg", || read_pg(&mut file))?;
386

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

            
389
            for (old_index, new_index) in mapping.iter().enumerate() {
390
                debug!("{} -> {:?}", old_index, new_index);
391
            }
392

            
393
            let mut output_file = File::create(&args.output)?;
394
            write_pg(&mut output_file, &reachable_game)?;
395
        }
396
        ParityGameFormat::VPG => {
397
            let manager_ref = oxidd::bdd::new_manager(
398
                cli.oxidd_node_capacity,
399
                cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
400
                cli.oxidd_workers,
401
            );
402

            
403
            let game = timing.measure("read_vpg", || read_vpg(&manager_ref, &mut file))?;
404
            let (reachable_game, mapping) = timing.measure("compute_reachable_vpg", || compute_reachable(&game));
405

            
406
            for (old_index, new_index) in mapping.iter().enumerate() {
407
                debug!("{} -> {:?}", old_index, new_index);
408
            }
409

            
410
            let mut output_file = File::create(&args.output)?;
411
            // Write reachable part using the PG writer, as reachable_game is a ParityGame.
412
            write_pg(&mut output_file, &reachable_game)?;
413
        }
414
    }
415

            
416
    Ok(())
417
}
418

            
419
/// Projects a feature transition system to a set of transition systems and writes them to output.
420
fn handle_project_fts(cli: &Cli, args: &ProjectArgs, timing: &mut Timing) -> Result<(), MercError> {
421
    let format = guess_lts_format_from_extension(&args.filename, args.format).ok_or_else(|| {
422
        format!(
423
            "Unknown featured transition system format for '{}'.",
424
            args.filename.display()
425
        )
426
    })?;
427

            
428
    if format != LtsFormat::Aut {
429
        return Err(MercError::from(
430
            "The project command only works for featured transition systems in the .aut format.",
431
        ));
432
    }
433

            
434
    // Read and solve a variability parity game.
435
    let manager_ref = oxidd::bdd::new_manager(
436
        cli.oxidd_node_capacity,
437
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
438
        cli.oxidd_workers,
439
    );
440

            
441
    // Read feature diagram
442
    let mut feature_diagram_file = File::open(&args.feature_diagram_filename).map_err(|e| {
443
        MercError::from(format!(
444
            "Could not open feature diagram file '{}': {}",
445
            &args.feature_diagram_filename.display(),
446
            e
447
        ))
448
    })?;
449
    let feature_diagram = FeatureDiagram::from_reader(&manager_ref, &mut feature_diagram_file)?;
450
    debug!("Feature diagram {:?}", feature_diagram);
451

            
452
    // Read the feature transition system.
453
    let mut fts_file = File::open(&args.filename)?;
454
    let fts = read_fts(&manager_ref, &mut fts_file, feature_diagram.features().clone())?;
455
    let output_path = Path::new(&args.output);
456

            
457
    for result in project_feature_transition_system_iter(&fts, &feature_diagram, timing) {
458
        let (ProjectedLts { bits, bdd: _, lts }, _) = result?;
459

            
460
        let extension = output_path.extension().ok_or("Missing extension on output file")?;
461
        let new_path = output_path
462
            .with_file_name(format!(
463
                "{}_{}",
464
                output_path
465
                    .file_stem()
466
                    .ok_or("Missing filename on output")?
467
                    .to_string_lossy(),
468
                FormatConfig(&bits)
469
            ))
470
            .with_added_extension(extension);
471
        let mut output_file = File::create(new_path)?;
472

            
473
        write_aut(&mut output_file, &lts)?;
474
    }
475

            
476
    Ok(())
477
}
478

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

            
484
    let mut file = File::open(&args.filename)?;
485
    if format != ParityGameFormat::VPG {
486
        return Err(MercError::from(
487
            "The project command only works for variability parity games.",
488
        ));
489
    }
490

            
491
    // Read the variability parity game.
492
    let manager_ref = oxidd::bdd::new_manager(
493
        cli.oxidd_node_capacity,
494
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
495
        cli.oxidd_workers,
496
    );
497

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

            
501
    for result in project_variability_parity_games_iter(&vpg, timing) {
502
        let (Projected { bits, bdd: _, game }, _) = result?;
503

            
504
        let extension = output_path.extension().ok_or("Missing extension on output file")?;
505
        let new_path = output_path
506
            .with_file_name(format!(
507
                "{}_{}",
508
                output_path
509
                    .file_stem()
510
                    .ok_or("Missing filename on output")?
511
                    .to_string_lossy(),
512
                FormatConfig(&bits)
513
            ))
514
            .with_added_extension(extension);
515

            
516
        let mut output_file = File::create(new_path)?;
517

            
518
        if args.reachable {
519
            let (reachable_pg, _projection) = compute_reachable(&game);
520
            write_pg(&mut output_file, &reachable_pg)?;
521
        } else {
522
            write_pg(&mut output_file, &game)?;
523
        }
524
    }
525

            
526
    Ok(())
527
}
528

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

            
541
    if format != LtsFormat::Aut {
542
        return Err(MercError::from(
543
            "The translate command only works for labelled transition systems in the .aut format.",
544
        ));
545
    }
546

            
547
    // Read LTS
548
    let mut lts_file = File::open(&args.labelled_transition_system).map_err(|e| {
549
        MercError::from(format!(
550
            "Could not open feature transition system file '{}': {}",
551
            &args.labelled_transition_system.display(),
552
            e
553
        ))
554
    })?;
555
    let lts = read_aut(&mut lts_file)?;
556

            
557
    // Read and validate formula (no actions/data specs supported here)
558
    let formula_spec = UntypedStateFrmSpec::parse(&read_to_string(&args.formula_filename).map_err(|e| {
559
        MercError::from(format!(
560
            "Could not open formula file '{}': {}",
561
            &args.formula_filename.display(),
562
            e
563
        ))
564
    })?)?;
565

            
566
    if !formula_spec.action_declarations.is_empty() {
567
        return Err(MercError::from("We do not support formulas with action declarations."));
568
    }
569

            
570
    if !formula_spec.data_specification.is_empty() {
571
        return Err(MercError::from("The formula must not contain a data specification."));
572
    }
573

            
574
    let vpg = translate(&lts, &formula_spec.formula)?;
575

            
576
    let mut output_file = File::create(&args.output)?;
577
    write_pg(&mut output_file, &vpg)?;
578

            
579
    Ok(())
580
}
581

            
582
/// Handle the `translate_vpg` subcommand.
583
///
584
/// Translates a feature diagram, a feature transition system (FTS), and a modal
585
/// formula into a variability parity game.
586
fn handle_translate_vpg(cli: &Cli, args: &TranslateVpgArgs) -> Result<(), MercError> {
587
    let manager_ref = oxidd::bdd::new_manager(
588
        cli.oxidd_node_capacity,
589
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
590
        cli.oxidd_workers,
591
    );
592

            
593
    // Read feature diagram
594
    let mut feature_diagram_file = File::open(&args.feature_diagram_filename).map_err(|e| {
595
        MercError::from(format!(
596
            "Could not open feature diagram file '{}': {}",
597
            &args.feature_diagram_filename.display(),
598
            e
599
        ))
600
    })?;
601
    let feature_diagram = FeatureDiagram::from_reader(&manager_ref, &mut feature_diagram_file)?;
602

            
603
    // Read FTS
604
    let mut fts_file = File::open(&args.fts_filename).map_err(|e| {
605
        MercError::from(format!(
606
            "Could not open feature transition system file '{}': {}",
607
            &args.fts_filename.display(),
608
            e
609
        ))
610
    })?;
611
    let fts = read_fts(&manager_ref, &mut fts_file, feature_diagram.features().clone())?;
612

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

            
625
    if !formula_spec.data_specification.is_empty() {
626
        return Err(MercError::from("The formula must not contain a data specification."));
627
    }
628

            
629
    let vpg = translate_vpg(
630
        &manager_ref,
631
        &fts,
632
        feature_diagram.configuration().clone(),
633
        &formula_spec.formula,
634
    )?;
635
    let mut output_file = File::create(&args.output)?;
636
    write_vpg(&mut output_file, &vpg)?;
637

            
638
    Ok(())
639
}
640

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

            
651
    if format == ParityGameFormat::PG {
652
        // Read and display a standard parity game.
653
        let game = timing.measure("read_pg", || read_pg(&mut file))?;
654

            
655
        let mut output_file = File::create(&args.output)?;
656
        write!(&mut output_file, "{}", PgDot::new(&game))?;
657
    } else {
658
        // Read and display a variability parity game.
659
        let manager_ref = oxidd::bdd::new_manager(
660
            cli.oxidd_node_capacity,
661
            cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
662
            cli.oxidd_workers,
663
        );
664

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

            
667
        let mut output_file = File::create(&args.output)?;
668
        write!(&mut output_file, "{}", VpgDot::new(&game))?;
669
    }
670

            
671
    if let Ok(dot_path) = which::which("dot") {
672
        info!("Generating PDF using dot...");
673
        cmd!(dot_path, "-Tpdf", &args.output, "-O").run()?;
674
    }
675

            
676
    Ok(())
677
}