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::process::ExitCode;
6

            
7
use clap::Parser;
8
use clap::Subcommand;
9
use duct::cmd;
10
use itertools::Itertools;
11
use log::debug;
12
use log::info;
13
use merc_vpg::make_vpg_total;
14
use merc_vpg::verify_variability_product_zielonka_solution;
15
use oxidd::BooleanFunction;
16

            
17
use merc_symbolic::CubeIterAll;
18
use merc_symbolic::FormatConfig;
19
use merc_syntax::UntypedStateFrmSpec;
20
use merc_tools::VerbosityFlag;
21
use merc_tools::Version;
22
use merc_tools::VersionFlag;
23
use merc_unsafety::print_allocator_metrics;
24
use merc_utilities::MercError;
25
use merc_utilities::Timing;
26
use merc_vpg::FeatureDiagram;
27
use merc_vpg::ParityGameFormat;
28
use merc_vpg::PgDot;
29
use merc_vpg::Player;
30
use merc_vpg::VpgDot;
31
use merc_vpg::ZielonkaVariant;
32
use merc_vpg::compute_reachable;
33
use merc_vpg::guess_format_from_extension;
34
use merc_vpg::project_variability_parity_games_iter;
35
use merc_vpg::read_fts;
36
use merc_vpg::read_pg;
37
use merc_vpg::read_vpg;
38
use merc_vpg::solve_variability_product_zielonka;
39
use merc_vpg::solve_variability_zielonka;
40
use merc_vpg::solve_zielonka;
41
use merc_vpg::translate;
42
use merc_vpg::write_pg;
43
use merc_vpg::write_vpg;
44

            
45
#[derive(clap::Parser, Debug)]
46
#[command(
47
    about = "A command line tool for variability parity games",
48
    arg_required_else_help = true
49
)]
50
struct Cli {
51
    #[command(flatten)]
52
    version: VersionFlag,
53

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

            
57
    #[arg(long, global = true)]
58
    timings: bool,
59

            
60
    #[arg(long, global = true, default_value_t = 1)]
61
    oxidd_workers: u32,
62

            
63
    #[arg(long, global = true, default_value_t = 2024)]
64
    oxidd_node_capacity: usize,
65

            
66
    #[arg(long, global = true)]
67
    oxidd_cache_capacity: Option<usize>,
68

            
69
    #[command(subcommand)]
70
    commands: Option<Commands>,
71
}
72

            
73
#[derive(Debug, Subcommand)]
74
enum Commands {
75
    Solve(SolveArgs),
76
    Reachable(ReachableArgs),
77
    Project(ProjectArgs),
78
    Translate(TranslateArgs),
79
    Display(DisplayArgs),
80
}
81

            
82
/// Arguments for solving a parity game
83
#[derive(clap::Args, Debug)]
84
struct SolveArgs {
85
    filename: String,
86

            
87
    /// The parity game file format
88
    #[arg(long)]
89
    format: Option<ParityGameFormat>,
90

            
91
    /// For variability parity games there are several ways for solving.
92
    #[arg(long)]
93
    solve_variant: Option<ZielonkaVariant>,
94

            
95
    /// Whether to output the solution for every single vertex, not just in the initial vertex.
96
    #[arg(long, default_value_t = false)]
97
    full_solution: bool,
98

            
99
    /// Whether to verify the solution after computing it
100
    #[arg(long, default_value_t = false)]
101
    verify_solution: bool,
102
}
103

            
104
/// Arguments for computing the reachable part of a parity game
105
#[derive(clap::Args, Debug)]
106
struct ReachableArgs {
107
    filename: String,
108

            
109
    output: String,
110

            
111
    #[arg(long, short)]
112
    format: Option<ParityGameFormat>,
113
}
114

            
115
/// Arguments for projecting a variability parity game
116
#[derive(clap::Args, Debug)]
117
struct ProjectArgs {
118
    filename: String,
119

            
120
    output: String,
121

            
122
    /// Whether to compute the reachable part after outputting each projection
123
    #[arg(long, short, default_value_t = false)]
124
    reachable: bool,
125

            
126
    #[arg(long, short)]
127
    format: Option<ParityGameFormat>,
128
}
129

            
130
/// Arguments for translating a feature transition system and a modal formula into a variability parity game
131
#[derive(clap::Args, Debug)]
132
struct TranslateArgs {
133
    /// The filename of the feature diagram
134
    feature_diagram_filename: String,
135

            
136
    /// The filename of the feature transition system
137
    fts_filename: String,
138

            
139
    /// The filename of the modal formula
140
    formula_filename: String,
141

            
142
    /// The variability parity game output filename
143
    output: String,
144
}
145

            
146
/// Arguments for displaying a (variability) parity game
147
#[derive(clap::Args, Debug)]
148
struct DisplayArgs {
149
    filename: String,
150

            
151
    /// The .dot file output filename
152
    output: String,
153

            
154
    /// The parity game file format
155
    #[arg(long, short)]
156
    format: Option<ParityGameFormat>,
157
}
158

            
159
fn main() -> Result<ExitCode, MercError> {
160
    let cli = Cli::parse();
161

            
162
    let mut timing = Timing::new();
163

            
164
    env_logger::Builder::new()
165
        .filter_level(cli.verbosity.log_level_filter())
166
        .parse_default_env()
167
        .init();
168

            
169
    if cli.version.into() {
170
        eprintln!("{}", Version);
171
        return Ok(ExitCode::SUCCESS);
172
    }
173

            
174
    if let Some(command) = &cli.commands {
175
        match command {
176
            Commands::Solve(args) => handle_solve(&cli, args, &mut timing)?,
177
            Commands::Reachable(args) => handle_reachable(&cli, args, &mut timing)?,
178
            Commands::Project(args) => handle_project(&cli, args, &mut timing)?,
179
            Commands::Translate(args) => handle_translate(&cli, args)?,
180
            Commands::Display(args) => handle_display(&cli, args, &mut timing)?,
181
        }
182
    }
183

            
184
    if cli.timings {
185
        timing.print();
186
    }
187

            
188
    print_allocator_metrics();
189
    if cfg!(feature = "merc_metrics") {
190
        oxidd::bdd::print_stats();
191
    }
192
    Ok(ExitCode::SUCCESS)
193
}
194

            
195
/// Handle the `solve` subcommand.
196
///
197
/// Reads either a standard parity game (PG) or a variability parity game (VPG)
198
/// based on the provided format or filename extension, then solves it using
199
/// Zielonka's algorithm.
200
fn handle_solve(cli: &Cli, args: &SolveArgs, timing: &mut Timing) -> Result<(), MercError> {
201
    let path = Path::new(&args.filename);
202
    let mut file = File::open(path)?;
203
    let format = guess_format_from_extension(path, args.format).ok_or("Unknown parity game file format.")?;
204

            
205
    if format == ParityGameFormat::PG {
206
        // Read and solve a standard parity game.
207
        let mut time_read = timing.start("read_pg");
208
        let game = read_pg(&mut file)?;
209
        time_read.finish();
210

            
211
        let mut time_solve = timing.start("solve_zielonka");
212
        let solution = solve_zielonka(&game);
213
        if args.full_solution {
214
            for (index, player_set) in solution.iter().enumerate() {
215
                println!("W{index}: {}", player_set.iter_ones().format(", "));
216
            }
217
        } else if solution[0][0] {
218
            println!("{}", Player::Even.solution())
219
        } else {
220
            println!("{}", Player::Odd.solution())
221
        }
222
        time_solve.finish();
223
    } else {
224
        let solve_variant = args
225
            .solve_variant
226
            .ok_or("For variability parity game solving a solving strategy should be selected")?;
227

            
228
        // Read and solve a variability parity game.
229
        let manager_ref = oxidd::bdd::new_manager(
230
            cli.oxidd_node_capacity,
231
            cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
232
            cli.oxidd_workers,
233
        );
234

            
235
        let mut time_read = timing.start("read_vpg");
236
        let game = read_vpg(&manager_ref, &mut file)?;
237
        time_read.finish();
238

            
239
        let game = if !game.is_total(&manager_ref)? {
240
            make_vpg_total(&manager_ref, &game)?
241
        } else {
242
            game
243
        };
244

            
245
        let mut time_solve = timing.start("solve_variability_zielonka");
246
        if solve_variant == ZielonkaVariant::Product {
247
            // Since we want to print W0, W1 separately, we need to store the results temporarily.
248
            let mut results = [Vec::new(), Vec::new()];
249
            for (cube, _bdd, solution) in solve_variability_product_zielonka(&game) {
250
                for (index, w) in solution.iter().enumerate() {
251
                    results[index].push((cube.clone(), w.clone()));
252
                }
253
            }
254

            
255
            for (index, w) in results.iter().enumerate() {
256
                println!("W{index}: ");
257

            
258
                for (cube, vertices) in w {
259
                    println!(
260
                        "For product {} the following vertices are in: {}",
261
                        FormatConfig(cube),
262
                        vertices
263
                            .iter_ones()
264
                            .filter(|v| if args.full_solution { true } else { *v == 0 })
265
                            .format(", ")
266
                    );
267
                }
268
            }
269
        } else {
270
            let solutions = solve_variability_zielonka(&manager_ref, &game, solve_variant, false)?;
271
            for (index, w) in solutions.iter().enumerate() {
272
                println!("W{index}: ");
273

            
274
                for entry in CubeIterAll::new(game.variables(), game.configuration()) {
275
                    let (config, config_function) = entry?;
276

            
277
                    println!(
278
                        "For product {} the following vertices are in: {}",
279
                        FormatConfig(&config),
280
                        w.iter() // Do not use iter_vertices because the first one is the initial vertex only
281
                            .take(if args.full_solution { usize::MAX } else { 1 }) // Take only first if we don't want full solution
282
                            .filter(|(_v, config)| { config.and(&config_function).unwrap().satisfiable() })
283
                            .map(|(v, _)| v)
284
                            .format(", ")
285
                    );
286
                }
287
            }
288

            
289
            if args.verify_solution {
290
                verify_variability_product_zielonka_solution(&game, &solutions)?;
291
            }
292
        }
293
        time_solve.finish();
294
    }
295

            
296
    Ok(())
297
}
298

            
299
/// Handle the `reachable` subcommand.
300
///
301
/// Reads a PG or VPG, computes its reachable part, and writes it to `output`.
302
/// Also logs the vertex index mapping to aid inspection.
303
fn handle_reachable(cli: &Cli, args: &ReachableArgs, timing: &mut Timing) -> Result<(), MercError> {
304
    let path = Path::new(&args.filename);
305
    let mut file = File::open(path)?;
306

            
307
    let format = guess_format_from_extension(path, args.format).ok_or("Unknown parity game file format.")?;
308

            
309
    match format {
310
        ParityGameFormat::PG => {
311
            let mut time_read = timing.start("read_pg");
312
            let game = read_pg(&mut file)?;
313
            time_read.finish();
314

            
315
            let mut time_reachable = timing.start("compute_reachable");
316
            let (reachable_game, mapping) = compute_reachable(&game);
317
            time_reachable.finish();
318

            
319
            for (old_index, new_index) in mapping.iter().enumerate() {
320
                debug!("{} -> {:?}", old_index, new_index);
321
            }
322

            
323
            let mut output_file = File::create(&args.output)?;
324
            write_pg(&mut output_file, &reachable_game)?;
325
        }
326
        ParityGameFormat::VPG => {
327
            let manager_ref = oxidd::bdd::new_manager(
328
                cli.oxidd_node_capacity,
329
                cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
330
                cli.oxidd_workers,
331
            );
332

            
333
            let mut time_read = timing.start("read_vpg");
334
            let game = read_vpg(&manager_ref, &mut file)?;
335
            time_read.finish();
336

            
337
            let mut time_reachable = timing.start("compute_reachable_vpg");
338
            let (reachable_game, mapping) = compute_reachable(&game);
339
            time_reachable.finish();
340

            
341
            for (old_index, new_index) in mapping.iter().enumerate() {
342
                debug!("{} -> {:?}", old_index, new_index);
343
            }
344

            
345
            let mut output_file = File::create(&args.output)?;
346
            // Write reachable part using the PG writer, as reachable_game is a ParityGame.
347
            write_pg(&mut output_file, &reachable_game)?;
348
        }
349
    }
350

            
351
    Ok(())
352
}
353

            
354
/// Compute all the projects of a variability parity game and write them to output.
355
fn handle_project(cli: &Cli, args: &ProjectArgs, timing: &mut Timing) -> Result<(), MercError> {
356
    let path = Path::new(&args.filename);
357
    let mut file = File::open(path)?;
358
    let format = guess_format_from_extension(path, args.format).ok_or("Unknown parity game file format.")?;
359

            
360
    if format != ParityGameFormat::VPG {
361
        return Err(MercError::from(
362
            "The project command only works for variability parity games.",
363
        ));
364
    }
365

            
366
    // Read the variability parity game.
367
    let manager_ref = oxidd::bdd::new_manager(
368
        cli.oxidd_node_capacity,
369
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
370
        cli.oxidd_workers,
371
    );
372

            
373
    let mut time_read = timing.start("read_vpg");
374
    let vpg = read_vpg(&manager_ref, &mut file)?;
375
    time_read.finish();
376

            
377
    let output_path = Path::new(&args.output);
378

            
379
    for result in project_variability_parity_games_iter(&vpg) {
380
        let (cube, _bdd, pg) = result?;
381

            
382
        let extension = output_path.extension().ok_or("Missing extension on output file")?;
383
        let new_path = output_path
384
            .with_file_name(format!(
385
                "{}_{}",
386
                output_path
387
                    .file_stem()
388
                    .ok_or("Missing filename on output")?
389
                    .to_string_lossy(),
390
                FormatConfig(&cube)
391
            ))
392
            .with_extension(extension);
393

            
394
        let mut output_file = File::create(new_path)?;
395

            
396
        if args.reachable {
397
            let (reachable_pg, _projection) = compute_reachable(&pg);
398
            write_pg(&mut output_file, &reachable_pg)?;
399
        } else {
400
            write_pg(&mut output_file, &pg)?;
401
        }
402
    }
403

            
404
    Ok(())
405
}
406

            
407
/// Handle the `translate` subcommand.
408
///
409
/// Translates a feature diagram, a feature transition system (FTS), and a modal
410
/// formula into a variability parity game.
411
fn handle_translate(cli: &Cli, args: &TranslateArgs) -> Result<(), MercError> {
412
    let manager_ref = oxidd::bdd::new_manager(
413
        cli.oxidd_node_capacity,
414
        cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
415
        cli.oxidd_workers,
416
    );
417

            
418
    // Read feature diagram
419
    let mut feature_diagram_file = File::open(&args.feature_diagram_filename).map_err(|e| {
420
        MercError::from(format!(
421
            "Could not open feature diagram file '{}': {}",
422
            &args.feature_diagram_filename, e
423
        ))
424
    })?;
425
    let feature_diagram = FeatureDiagram::from_reader(&manager_ref, &mut feature_diagram_file)?;
426

            
427
    // Read FTS
428
    let mut fts_file = File::open(&args.fts_filename).map_err(|e| {
429
        MercError::from(format!(
430
            "Could not open feature transition system file '{}': {}",
431
            &args.fts_filename, e
432
        ))
433
    })?;
434
    let fts = read_fts(&manager_ref, &mut fts_file, feature_diagram.features().clone())?;
435

            
436
    // Read and validate formula (no actions/data specs supported here)
437
    let formula_spec = UntypedStateFrmSpec::parse(&read_to_string(&args.formula_filename).map_err(|e| {
438
        MercError::from(format!(
439
            "Could not open formula file '{}': {}",
440
            &args.formula_filename, e
441
        ))
442
    })?)?;
443
    if !formula_spec.action_declarations.is_empty() {
444
        return Err(MercError::from("We do not support formulas with action declarations."));
445
    }
446

            
447
    if !formula_spec.data_specification.is_empty() {
448
        return Err(MercError::from("The formula must not contain a data specification."));
449
    }
450

            
451
    let vpg = translate(
452
        &manager_ref,
453
        &fts,
454
        feature_diagram.configuration().clone(),
455
        &formula_spec.formula,
456
    )?;
457
    let mut output_file = File::create(&args.output)?;
458
    write_vpg(&mut output_file, &vpg)?;
459

            
460
    Ok(())
461
}
462

            
463
/// Handle the `display` subcommand.
464
///
465
/// Reads a PG or VPG and writes a Graphviz `.dot` representation to `output`.
466
/// If the `dot` tool is available, also generates a PDF (`output.pdf`).
467
fn handle_display(cli: &Cli, args: &DisplayArgs, timing: &mut Timing) -> Result<(), MercError> {
468
    let path = Path::new(&args.filename);
469
    let mut file = File::open(path)?;
470
    let format = guess_format_from_extension(path, args.format).ok_or("Unknown parity game file format.")?;
471

            
472
    if format == ParityGameFormat::PG {
473
        // Read and display a standard parity game.
474
        let mut time_read = timing.start("read_pg");
475
        let game = read_pg(&mut file)?;
476
        time_read.finish();
477

            
478
        let mut output_file = File::create(&args.output)?;
479
        write!(&mut output_file, "{}", PgDot::new(&game))?;
480
    } else {
481
        // Read and display a variability parity game.
482
        let manager_ref = oxidd::bdd::new_manager(
483
            cli.oxidd_node_capacity,
484
            cli.oxidd_cache_capacity.unwrap_or(cli.oxidd_node_capacity),
485
            cli.oxidd_workers,
486
        );
487

            
488
        let mut time_read = timing.start("read_vpg");
489
        let game = read_vpg(&manager_ref, &mut file)?;
490
        time_read.finish();
491

            
492
        let mut output_file = File::create(&args.output)?;
493
        write!(&mut output_file, "{}", VpgDot::new(&game))?;
494
    }
495

            
496
    if let Ok(dot_path) = which::which("dot") {
497
        info!("Generating PDF using dot...");
498
        cmd!(dot_path, "-Tpdf", &args.output, "-O").run()?;
499
    }
500

            
501
    Ok(())
502
}