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::project_feature_transition_system_iter;
22
use merc_vpg::verify_solution;
23
use oxidd::BooleanFunction;
24

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

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

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

            
68
    #[command(flatten)]
69
    verbosity: VerbosityFlag,
70

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

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

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

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

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

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

            
98
/// Solve a parity game
99
#[derive(clap::Args, Debug)]
100
struct SolveArgs {
101
    filename: String,
102

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

            
107
    /// Sets the solving variant used for variability parity games
108
    #[arg(long)]
109
    solve_variant: Option<ZielonkaVariant>,
110

            
111
    /// Output the solution for every single vertex instead of only the initial vertex.
112
    #[arg(long, default_value_t = false)]
113
    full_solution: bool,
114

            
115
    /// Whether to verify the solution after computing it
116
    #[arg(long, default_value_t = false)]
117
    verify_solution: bool,
118
}
119

            
120
/// Compute the reachable part of a parity game
121
#[derive(clap::Args, Debug)]
122
struct ReachableArgs {
123
    /// The filename of the parity game to compute the reachable part of
124
    filename: PathBuf,
125

            
126
    /// The output filename for the reachable part of the parity game
127
    output: PathBuf,
128

            
129
    #[arg(long, short)]
130
    format: Option<ParityGameFormat>,
131
}
132

            
133
/// Project a feature transition system to a set of transition systems
134
#[derive(clap::Args, Debug)]
135
struct ProjectArgs {
136
    /// The filename of the variability parity game to project
137
    filename: PathBuf,
138

            
139
    /// The filename of the feature diagram
140
    feature_diagram_filename: PathBuf,
141

            
142
    /// The output filename pattern for the projected labelled transition systems.
143
    output: String,
144

            
145
    /// The input featured transition system file format
146
    #[arg(long, short)]
147
    format: Option<LtsFormat>,
148
}
149

            
150
/// Project a variability parity game to a set of parity games
151
#[derive(clap::Args, Debug)]
152
struct ProjectVpgArgs {
153
    /// The filename of the variability parity game to project
154
    filename: PathBuf,
155

            
156
    /// The output filename pattern for the projected parity games.
157
    output: String,
158

            
159
    /// Whether to compute the reachable part of each projection.
160
    #[arg(long, short)]
161
    reachable: bool,
162

            
163
    #[arg(long, short)]
164
    format: Option<ParityGameFormat>,
165
}
166

            
167
/// Translate a labelled transition system and a modal formula into a parity game
168
#[derive(clap::Args, Debug)]
169
struct TranslateArgs {
170
    /// The filename of the labelled transition system
171
    labelled_transition_system: PathBuf,
172

            
173
    /// The input labelled transition system file format
174
    #[arg(long, short)]
175
    format: Option<LtsFormat>,
176

            
177
    /// The filename of the modal formula
178
    formula_filename: PathBuf,
179

            
180
    /// The parity game output filename
181
    output: PathBuf,
182
}
183

            
184
/// Translate a feature transition system and a modal formula into a variability parity game
185
#[derive(clap::Args, Debug)]
186
struct TranslateVpgArgs {
187
    /// The filename of the feature diagram
188
    feature_diagram_filename: PathBuf,
189

            
190
    /// The input featured transition system file format
191
    #[arg(long, short)]
192
    format: Option<LtsFormat>,
193

            
194
    /// The filename of the feature transition system
195
    fts_filename: PathBuf,
196

            
197
    /// The filename of the modal formula
198
    formula_filename: PathBuf,
199

            
200
    /// The variability parity game output filename
201
    output: String,
202
}
203

            
204
/// Display a (variability) parity game
205
#[derive(clap::Args, Debug)]
206
struct DisplayArgs {
207
    /// The filename of the (variability) parity game to display
208
    filename: PathBuf,
209

            
210
    /// The .dot file output filename
211
    output: PathBuf,
212

            
213
    /// The parity game file format
214
    #[arg(long, short)]
215
    format: Option<ParityGameFormat>,
216
}
217

            
218
fn main() -> Result<ExitCode, MercError> {
219
    let cli = Cli::parse();
220

            
221
    let mut timing = Timing::new();
222

            
223
    env_logger::Builder::new()
224
        .filter_level(cli.verbosity.log_level_filter())
225
        .format_key_values(|formatter, source| format_key_values_json(formatter, source))
226
        .parse_default_env()
227
        .init();
228

            
229
    if cli.version.into() {
230
        eprintln!("{}", Version);
231
        return Ok(ExitCode::SUCCESS);
232
    }
233

            
234
    if let Some(command) = &cli.commands {
235
        match command {
236
            Commands::Solve(args) => handle_solve(&cli, args, &mut timing)?,
237
            Commands::Reachable(args) => handle_reachable(&cli, args, &mut timing)?,
238
            Commands::Project(args) => handle_project_fts(&cli, args, &mut timing)?,
239
            Commands::ProjectVpg(args) => handle_project_vpg(&cli, args, &mut timing)?,
240
            Commands::Translate(args) => handle_translate(args)?,
241
            Commands::TranslateVpg(args) => handle_translate_vpg(&cli, args)?,
242
            Commands::Display(args) => handle_display(&cli, args, &mut timing)?,
243
        }
244
    }
245

            
246
    if cli.timings {
247
        timing.print();
248
    }
249

            
250
    print_allocator_metrics();
251
    if cfg!(feature = "merc_metrics") {
252
        oxidd::bdd::print_stats();
253
    }
254
    Ok(ExitCode::SUCCESS)
255
}
256

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

            
268
    if format == ParityGameFormat::PG {
269
        // Read and solve a standard parity game.
270
        let game = timing.measure("read_pg", || read_pg(&mut file))?;
271

            
272
        let (solution, strategy) = timing.measure("solve_zielonka", || solve_zielonka(&game, args.verify_solution));
273
        if args.full_solution {
274
            for (index, player_set) in solution.iter().enumerate() {
275
                println!("W{index}: {}", player_set.iter_ones().format(", "));
276
            }
277
        } else if solution[0][0] {
278
            println!("{}", Player::Even.solution())
279
        } else {
280
            println!("{}", Player::Odd.solution())
281
        }
282

            
283
        if let Some(strategy) = strategy && args.verify_solution {
284
            verify_solution(&game, &solution, &strategy);
285
        }
286
    } else {
287
        let solve_variant = args
288
            .solve_variant
289
            .ok_or("For variability parity game solving a solving strategy should be selected")?;
290

            
291
        // Read and solve a variability parity game.
292
        let manager_ref = oxidd::bdd::new_manager(
293
            cli.oxidd_node_capacity,
294
            cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
295
            cli.oxidd_workers,
296
        );
297

            
298
        let game = timing.measure("read_vpg", || -> Result<_, MercError> {
299
            read_vpg(&manager_ref, &mut file)
300
        })?;
301

            
302
        let game = if !game.is_total(&manager_ref)? {
303
            info!("Making the VPG total...");
304
            make_vpg_total(&manager_ref, &game)?
305
        } else {
306
            game
307
        };
308

            
309
        timing.measure("solve_variability_zielonka", || -> Result<_, MercError> {
310
            if solve_variant == ZielonkaVariant::Product {
311
                // Since we want to print W0, W1 separately, we need to store the results temporarily.
312
                let mut results = [Vec::new(), Vec::new()];
313
                for result in solve_variability_product_zielonka(&game, timing) {
314
                    let (cube, _bdd, solution) = result?;
315

            
316
                    for (index, w) in solution.iter().enumerate() {
317
                        results[index].push((cube.clone(), w.clone()));
318
                    }
319
                }
320

            
321
                for (index, w) in results.iter().enumerate() {
322
                    for (cube, vertices) in w {
323
                        println!(
324
                            "W{index}: For product {} the following vertices are in: {}",
325
                            FormatConfig(cube),
326
                            vertices
327
                                .iter_ones()
328
                                .filter(|v| if args.full_solution { true } else { *v == 0 })
329
                                .format(", ")
330
                        );
331
                    }
332
                }
333
            } else {
334
                let solutions = solve_variability_zielonka(&manager_ref, &game, solve_variant, false)?;
335
                for (index, w) in solutions.iter().enumerate() {
336
                    for entry in CubeIterAll::new(game.configuration()) {
337
                        let config = entry?;
338
                        let config_function = bits_to_bdd(&manager_ref, game.variables(), &config)?;
339

            
340
                        println!(
341
                            "W{index}: For product {} the following vertices are in: {}",
342
                            FormatConfig(&config),
343
                            w.iter() // Do not use iter_vertices because the first one is the initial vertex only
344
                                .take(if args.full_solution { usize::MAX } else { 1 }) // Take only first if we don't want full solution
345
                                .filter(|(_v, config)| { config.and(&config_function).unwrap().satisfiable() })
346
                                .map(|(v, _)| v)
347
                                .format(", ")
348
                        );
349
                    }
350
                }
351

            
352
                if args.verify_solution {
353
                    verify_variability_product_zielonka_solution(&game, &solutions, timing)?;
354
                }
355
            }
356

            
357
            Ok(())
358
        })?;
359
    }
360

            
361
    Ok(())
362
}
363

            
364
/// Handle the `reachable` subcommand.
365
///
366
/// Reads a PG or VPG, computes its reachable part, and writes it to `output`.
367
/// Also logs the vertex index mapping to aid inspection.
368
fn handle_reachable(cli: &Cli, args: &ReachableArgs, timing: &mut Timing) -> Result<(), MercError> {
369
    let path = Path::new(&args.filename);
370
    let format = guess_format_from_extension(path, args.format)
371
        .ok_or_else(|| format!("Unknown parity game file format for '{}", path.display()))?;
372

            
373
    let mut file = File::open(path)?;
374
    match format {
375
        ParityGameFormat::PG => {
376
            let game = timing.measure("read_pg", || read_pg(&mut file))?;
377

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

            
380
            for (old_index, new_index) in mapping.iter().enumerate() {
381
                debug!("{} -> {:?}", old_index, new_index);
382
            }
383

            
384
            let mut output_file = File::create(&args.output)?;
385
            write_pg(&mut output_file, &reachable_game)?;
386
        }
387
        ParityGameFormat::VPG => {
388
            let manager_ref = oxidd::bdd::new_manager(
389
                cli.oxidd_node_capacity,
390
                cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
391
                cli.oxidd_workers,
392
            );
393

            
394
            let game = timing.measure("read_vpg", || read_vpg(&manager_ref, &mut file))?;
395
            let (reachable_game, mapping) = timing.measure("compute_reachable_vpg", || compute_reachable(&game));
396

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

            
401
            let mut output_file = File::create(&args.output)?;
402
            // Write reachable part using the PG writer, as reachable_game is a ParityGame.
403
            write_pg(&mut output_file, &reachable_game)?;
404
        }
405
    }
406

            
407
    Ok(())
408
}
409

            
410
/// Projects a feature transition system to a set of transition systems and writes them to output.
411
fn handle_project_fts(cli: &Cli, args: &ProjectArgs, timing: &mut Timing) -> Result<(), MercError> {
412
    let format = guess_lts_format_from_extension(&args.filename, args.format)
413
        .ok_or_else(|| format!("Unknown featured transition system format for '{}'.", args.filename.display()))?;
414

            
415
    if format != LtsFormat::Aut {
416
        return Err(MercError::from(
417
            "The project command only works for featured transition systems in the .aut format.",
418
        ));
419
    }
420

            
421
    // Read and solve a variability parity game.
422
    let manager_ref = oxidd::bdd::new_manager(
423
        cli.oxidd_node_capacity,
424
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
425
        cli.oxidd_workers,
426
    );
427

            
428
    // Read feature diagram
429
    let mut feature_diagram_file = File::open(&args.feature_diagram_filename).map_err(|e| {
430
        MercError::from(format!(
431
            "Could not open feature diagram file '{}': {}",
432
            &args.feature_diagram_filename.display(),
433
            e
434
        ))
435
    })?;
436
    let feature_diagram = FeatureDiagram::from_reader(&manager_ref, &mut feature_diagram_file)?;
437
    debug!("Feature diagram {:?}", feature_diagram);
438

            
439
    // Read the feature transition system.
440
    let mut fts_file = File::open(&args.filename)?;
441
    let fts = read_fts(&manager_ref, &mut fts_file, feature_diagram.features().clone())?;
442
    let output_path = Path::new(&args.output);
443

            
444
    for result in project_feature_transition_system_iter(&fts, &feature_diagram, timing) {
445
        let (ProjectedLts { bits, bdd: _, lts }, _) = result?;
446

            
447
        let extension = output_path.extension().ok_or("Missing extension on output file")?;
448
        let new_path = output_path
449
            .with_file_name(format!(
450
                "{}_{}",
451
                output_path
452
                    .file_stem()
453
                    .ok_or("Missing filename on output")?
454
                    .to_string_lossy(),
455
                FormatConfig(&bits)
456
            ))
457
            .with_added_extension(extension);
458
        let mut output_file = File::create(new_path)?;
459

            
460
        write_aut(&mut output_file, &lts)?;
461
    }
462

            
463
    Ok(())
464
}
465

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

            
471
    let mut file = File::open(&args.filename)?;
472
    if format != ParityGameFormat::VPG {
473
        return Err(MercError::from(
474
            "The project command only works for variability parity games.",
475
        ));
476
    }
477

            
478
    // Read the variability parity game.
479
    let manager_ref = oxidd::bdd::new_manager(
480
        cli.oxidd_node_capacity,
481
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
482
        cli.oxidd_workers,
483
    );
484

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

            
488
    for result in project_variability_parity_games_iter(&vpg, timing) {
489
        let (Projected { bits, bdd: _, game }, _) = result?;
490

            
491
        let extension = output_path.extension().ok_or("Missing extension on output file")?;
492
        let new_path = output_path
493
            .with_file_name(format!(
494
                "{}_{}",
495
                output_path
496
                    .file_stem()
497
                    .ok_or("Missing filename on output")?
498
                    .to_string_lossy(),
499
                FormatConfig(&bits)
500
            ))
501
            .with_added_extension(extension);
502

            
503
        let mut output_file = File::create(new_path)?;
504

            
505
        if args.reachable {
506
            let (reachable_pg, _projection) = compute_reachable(&game);
507
            write_pg(&mut output_file, &reachable_pg)?;
508
        } else {
509
            write_pg(&mut output_file, &game)?;
510
        }
511
    }
512

            
513
    Ok(())
514
}
515

            
516
/// Handle the `translate` subcommand.
517
///
518
/// Translates a feature diagram, a feature transition system (FTS), and a modal
519
/// formula into a variability parity game.
520
fn handle_translate(args: &TranslateArgs) -> Result<(), MercError> {
521
    let format = guess_lts_format_from_extension(&args.labelled_transition_system, args.format).ok_or_else(|| {
522
        format!(
523
            "Unknown labelled transition system format for '{}'.",
524
            args.labelled_transition_system.display()
525
        )
526
    })?;
527

            
528
    if format != LtsFormat::Aut {
529
        return Err(MercError::from(
530
            "The translate command only works for labelled transition systems in the .aut format.",
531
        ));
532
    }
533

            
534
    // Read LTS
535
    let mut lts_file = File::open(&args.labelled_transition_system).map_err(|e| {
536
        MercError::from(format!(
537
            "Could not open feature transition system file '{}': {}",
538
            &args.labelled_transition_system.display(),
539
            e
540
        ))
541
    })?;
542
    let lts = read_aut(&mut lts_file, Vec::new())?;
543

            
544
    // Read and validate formula (no actions/data specs supported here)
545
    let formula_spec = UntypedStateFrmSpec::parse(&read_to_string(&args.formula_filename).map_err(|e| {
546
        MercError::from(format!(
547
            "Could not open formula file '{}': {}",
548
            &args.formula_filename.display(),
549
            e
550
        ))
551
    })?)?;
552

            
553
    if !formula_spec.action_declarations.is_empty() {
554
        return Err(MercError::from("We do not support formulas with action declarations."));
555
    }
556

            
557
    if !formula_spec.data_specification.is_empty() {
558
        return Err(MercError::from("The formula must not contain a data specification."));
559
    }
560

            
561
    let vpg = translate(&lts, &formula_spec.formula)?;
562

            
563
    let mut output_file = File::create(&args.output)?;
564
    write_pg(&mut output_file, &vpg)?;
565

            
566
    Ok(())
567
}
568

            
569
/// Handle the `translate_vpg` subcommand.
570
///
571
/// Translates a feature diagram, a feature transition system (FTS), and a modal
572
/// formula into a variability parity game.
573
fn handle_translate_vpg(cli: &Cli, args: &TranslateVpgArgs) -> Result<(), MercError> {
574
    let manager_ref = oxidd::bdd::new_manager(
575
        cli.oxidd_node_capacity,
576
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
577
        cli.oxidd_workers,
578
    );
579

            
580
    // Read feature diagram
581
    let mut feature_diagram_file = File::open(&args.feature_diagram_filename).map_err(|e| {
582
        MercError::from(format!(
583
            "Could not open feature diagram file '{}': {}",
584
            &args.feature_diagram_filename.display(),
585
            e
586
        ))
587
    })?;
588
    let feature_diagram = FeatureDiagram::from_reader(&manager_ref, &mut feature_diagram_file)?;
589

            
590
    // Read FTS
591
    let mut fts_file = File::open(&args.fts_filename).map_err(|e| {
592
        MercError::from(format!(
593
            "Could not open feature transition system file '{}': {}",
594
            &args.fts_filename.display(),
595
            e
596
        ))
597
    })?;
598
    let fts = read_fts(&manager_ref, &mut fts_file, feature_diagram.features().clone())?;
599

            
600
    // Read and validate formula (no actions/data specs supported here)
601
    let formula_spec = UntypedStateFrmSpec::parse(&read_to_string(&args.formula_filename).map_err(|e| {
602
        MercError::from(format!(
603
            "Could not open formula file '{}': {}",
604
            &args.formula_filename.display(),
605
            e
606
        ))
607
    })?)?;
608
    if !formula_spec.action_declarations.is_empty() {
609
        return Err(MercError::from("We do not support formulas with action declarations."));
610
    }
611

            
612
    if !formula_spec.data_specification.is_empty() {
613
        return Err(MercError::from("The formula must not contain a data specification."));
614
    }
615

            
616
    let vpg = translate_vpg(
617
        &manager_ref,
618
        &fts,
619
        feature_diagram.configuration().clone(),
620
        &formula_spec.formula,
621
    )?;
622
    let mut output_file = File::create(&args.output)?;
623
    write_vpg(&mut output_file, &vpg)?;
624

            
625
    Ok(())
626
}
627

            
628
/// Handle the `display` subcommand.
629
///
630
/// Reads a PG or VPG and writes a Graphviz `.dot` representation to `output`.
631
/// If the `dot` tool is available, also generates a PDF (`output.pdf`).
632
fn handle_display(cli: &Cli, args: &DisplayArgs, timing: &mut Timing) -> Result<(), MercError> {
633
    let path = Path::new(&args.filename);
634
    let mut file = File::open(path)?;
635
    let format = guess_format_from_extension(path, args.format)
636
        .ok_or_else(|| format!("Unknown parity game file format for '{}'.", path.display()))?;
637

            
638
    if format == ParityGameFormat::PG {
639
        // Read and display a standard parity game.
640
        let game = timing.measure("read_pg", || read_pg(&mut file))?;
641

            
642
        let mut output_file = File::create(&args.output)?;
643
        write!(&mut output_file, "{}", PgDot::new(&game))?;
644
    } else {
645
        // Read and display a variability parity game.
646
        let manager_ref = oxidd::bdd::new_manager(
647
            cli.oxidd_node_capacity,
648
            cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
649
            cli.oxidd_workers,
650
        );
651

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

            
654
        let mut output_file = File::create(&args.output)?;
655
        write!(&mut output_file, "{}", VpgDot::new(&game))?;
656
    }
657

            
658
    if let Ok(dot_path) = which::which("dot") {
659
        info!("Generating PDF using dot...");
660
        cmd!(dot_path, "-Tpdf", &args.output, "-O").run()?;
661
    }
662

            
663
    Ok(())
664
}