1use std::path::PathBuf;
2
3use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
4
5use clap_complete::Shell;
6use conjure_cp::settings::{
7 Channelling, DEFAULT_HEURISTIC_SEED, Heuristic, Parser as InputParser, QuantifiedExpander,
8 Rewriter, SolverFamily,
9};
10use conjure_cp::solver::adaptors::{MinionValueOrder, MinionVariableOrder, SatBackend};
11use git_version::git_version;
12
13use crate::{pretty, solve, test_solve};
14
15pub(crate) const LOGGING_HELP_HEADING: Option<&str> = Some("Logging & Output");
16pub(crate) const CONFIGURATION_HELP_HEADING: Option<&str> = Some("Configuration");
17pub(crate) const MODELLING_HELP_HEADING: Option<&str> = Some("Modelling choices");
18
19#[derive(Clone, Debug, Subcommand)]
21pub enum Command {
22 Solve(solve::Args),
24 PrintJsonSchema,
26 TestSolve(test_solve::Args),
31 Completion(CompletionArgs),
33 Pretty(pretty::Args),
34 ServerLSP,
36}
37
38#[derive(Clone, Debug, Parser)]
40#[command(
41 author,
42 about = "Conjure Oxide: Automated Constraints Modelling Toolkit",
43 before_help = "Full documentation can be found online at: https://conjure-cp.github.io/conjure-oxide",
44 disable_help_flag = true,
46 version = git_version!(),
47 disable_version_flag = true,
48 display_name = "conjure-oxide",
49 arg_required_else_help = false
52)]
53pub struct Cli {
54 #[command(subcommand)]
55 pub subcommand: Command,
56
57 #[command(flatten)]
58 pub global_args: GlobalArgs,
59
60 #[arg(long = "version", short = 'V', action = ArgAction::Version)]
64 pub version: (),
65}
66
67#[derive(Debug, Clone, Args)]
68pub struct GlobalArgs {
69 #[arg(long, action = clap::ArgAction::Help, global = true)]
71 pub help: (),
72
73 #[arg(long, value_name = "EXTRA_RULE_SETS", global = true)]
75 pub extra_rule_sets: Vec<String>,
76
77 #[arg(
81 long,
82 short = 'v',
83 action = ArgAction::Count,
84 global = true,
85 conflicts_with = "quiet",
86 help_heading = LOGGING_HELP_HEADING
87 )]
88 pub verbose: u8,
89
90 #[arg(long, short = 'q', global = true, help_heading = LOGGING_HELP_HEADING)]
92 pub quiet: bool,
93
94 #[arg(long, global = true, help_heading=LOGGING_HELP_HEADING)]
96 pub rule_trace: Option<PathBuf>,
97
98 #[arg(long, global = true, help_heading=LOGGING_HELP_HEADING)]
103 pub rule_trace_aggregates: Option<PathBuf>,
104
105 #[arg(long, default_value_t = false, global = true, help_heading=LOGGING_HELP_HEADING)]
110 pub rule_trace_cdp: bool,
111
112 #[arg(
116 long = "rule-attempt-trace",
117 global = true,
118 help_heading=LOGGING_HELP_HEADING
119 )]
120 pub rule_attempt_trace: Option<PathBuf>,
121
122 #[arg(
126 long,
127 default_value_t = InputParser::default(),
128 value_parser = parse_parser,
129 global = true,
130 help_heading = CONFIGURATION_HELP_HEADING
131 )]
132 pub parser: InputParser,
133
134 #[arg(long, default_value_t = Rewriter::default(), value_parser = parse_rewriter, global = true, help_heading = CONFIGURATION_HELP_HEADING)]
144 pub rewriter: Rewriter,
145
146 #[arg(
152 long,
153 default_value_t = QuantifiedExpander::Auto,
154 value_parser = parse_comprehension_expander,
155 global = true,
156 help_heading = MODELLING_HELP_HEADING
157 )]
158 pub comprehension_expander: QuantifiedExpander,
159
160 #[arg(
169 long,
170 short = 'h',
171 default_value_t = Heuristic::Compact,
172 value_parser = parse_cli_heuristic,
173 global = true,
174 help_heading = MODELLING_HELP_HEADING
175 )]
176 pub heuristic: Heuristic,
177
178 #[arg(
183 long,
184 value_name = "INTS",
185 value_delimiter = ',',
186 global = true,
187 help_heading = MODELLING_HELP_HEADING
188 )]
189 pub responses: Vec<usize>,
190
191 #[arg(
193 long,
194 default_value_t = DEFAULT_HEURISTIC_SEED,
195 global = true,
196 help_heading = MODELLING_HELP_HEADING
197 )]
198 pub seed: u64,
199
200 #[arg(
202 long,
203 default_value_t = 0,
204 global = true,
205 help_heading = CONFIGURATION_HELP_HEADING
206 )]
207 pub solver_seed: u32,
208
209 #[arg(
216 long,
217 default_value_t = Channelling::No,
218 value_parser = parse_cli_channelling,
219 global = true,
220 help_heading = MODELLING_HELP_HEADING
221 )]
222 pub channelling: Channelling,
223
224 #[arg(long = "sat-encoding-element", global = true, help_heading = MODELLING_HELP_HEADING)]
226 pub element_encoding: Option<conjure_cp::ast::sat_decision::ElementEncoding>,
227
228 #[arg(long = "sat-encoding-table", global = true, help_heading = MODELLING_HELP_HEADING)]
230 pub table_encoding: Option<conjure_cp::ast::sat_decision::TableEncoding>,
231
232 #[arg(long = "sat-encoding-alldifferent", global = true, help_heading = MODELLING_HELP_HEADING)]
234 pub alldifferent_encoding: Option<conjure_cp::ast::sat_decision::AllDifferentEncoding>,
235
236 #[arg(long = "sat-encoding-amo", global = true, help_heading = MODELLING_HELP_HEADING)]
240 pub amo_encoding: Option<conjure_cp::ast::sat_decision::AmoEncoding>,
241
242 #[arg(long = "sat-encoding-cardinality", global = true, help_heading = MODELLING_HELP_HEADING)]
244 pub cardinality_encoding: Option<conjure_cp::ast::sat_decision::CardinalityEncoding>,
245
246 #[arg(long = "sat-encoding-pb", global = true, help_heading = MODELLING_HELP_HEADING)]
248 pub pb_encoding: Option<conjure_cp::ast::sat_decision::PbEncoding>,
249
250 #[arg(
259 long,
260 value_name = "SOLVER",
261 value_parser = parse_solver_family,
262 default_value = "minion",
263 short = 's',
264 global = true,
265 help_heading = CONFIGURATION_HELP_HEADING
266 )]
267 pub solver: SolverFamily,
268
269 #[arg(long, value_enum, default_value_t = SatBackend::default(), global = true,
271 help_heading = CONFIGURATION_HELP_HEADING)]
272 pub sat_backend: SatBackend,
273
274 #[arg(
280 long,
281 global = true,
282 help_heading = MODELLING_HELP_HEADING
283 )]
284 pub minion_discrete_threshold: Option<usize>,
285
286 #[arg(
291 long,
292 value_name = "ORDER",
293 value_parser = parse_minion_variable_order,
294 global = true,
295 help_heading = CONFIGURATION_HELP_HEADING
296 )]
297 pub minion_varorder: Option<MinionVariableOrder>,
298
299 #[arg(
303 long,
304 value_name = "ORDER",
305 value_parser = parse_minion_value_order,
306 global = true,
307 help_heading = CONFIGURATION_HELP_HEADING
308 )]
309 pub minion_valorder: Option<MinionValueOrder>,
310
311 #[arg(long,global=true, value_names=["filename"], next_line_help=true, help_heading=LOGGING_HELP_HEADING)]
320 pub save_solver_input_file: Option<PathBuf>,
321
322 #[arg(long, global = true, help_heading = CONFIGURATION_HELP_HEADING)]
326 pub solver_timeout: Option<humantime::Duration>,
327
328 #[arg(long, value_name = "PATH", global = true, help_heading = LOGGING_HELP_HEADING)]
330 pub log_file: Option<PathBuf>,
331
332 #[arg(long, value_enum, requires = "log_file", global = true, help_heading = LOGGING_HELP_HEADING)]
334 pub log_format: Option<LogFormat>,
335
336 #[arg(long, value_enum, requires = "log_file", global = true, help_heading = LOGGING_HELP_HEADING)]
338 pub log_detail: Option<LogDetail>,
339}
340
341#[derive(Clone, Copy, Debug, Default, ValueEnum)]
342pub enum LogFormat {
343 #[default]
344 Text,
345 Json,
346}
347
348#[derive(Clone, Copy, Debug, Default, ValueEnum)]
349pub enum LogDetail {
350 #[default]
351 Stages,
352 Applications,
353 Attempts,
354}
355
356#[derive(Debug, Clone, Args)]
357pub struct CompletionArgs {
358 #[arg(value_enum)]
360 pub shell: Shell,
361}
362
363#[derive(Debug, Clone, Copy, clap::ValueEnum)]
364pub enum ShellTypes {
365 Bash,
366 Zsh,
367 Fish,
368 PowerShell,
369 Elvish,
370}
371
372fn parse_comprehension_expander(input: &str) -> Result<QuantifiedExpander, String> {
373 input.parse()
374}
375
376fn parse_cli_heuristic(input: &str) -> Result<Heuristic, String> {
377 match input.parse::<Heuristic>()? {
378 Heuristic::All => {
379 Err("heuristic 'x' (all) is not supported by the command line yet".to_string())
380 }
381 heuristic => Ok(heuristic),
382 }
383}
384
385fn parse_cli_channelling(input: &str) -> Result<Channelling, String> {
386 input.parse::<Channelling>()
387}
388
389fn parse_rewriter(input: &str) -> Result<Rewriter, String> {
390 input.parse::<Rewriter>()
391}
392
393fn parse_solver_family(input: &str) -> Result<SolverFamily, String> {
394 let family = input.parse()?;
395 if family == SolverFamily::Z3 && !cfg!(feature = "z3") {
396 return Err("Z3 solver support was not compiled in (enable the `z3` feature).".into());
397 }
398 Ok(family)
399}
400
401fn parse_parser(input: &str) -> Result<InputParser, String> {
402 input.parse()
403}
404
405fn parse_minion_value_order(input: &str) -> Result<MinionValueOrder, String> {
406 match input {
407 "ascend" => Ok(MinionValueOrder::Ascend),
408 "descend" => Ok(MinionValueOrder::Descend),
409 "random" => Ok(MinionValueOrder::Random),
410 other => Err(format!(
411 "unknown minion value order '{other}', expected one of: ascend, descend, random"
412 )),
413 }
414}
415
416fn parse_minion_variable_order(input: &str) -> Result<MinionVariableOrder, String> {
417 match input {
418 "static" => Ok(MinionVariableOrder::Static),
419 "sdf" => Ok(MinionVariableOrder::SmallestDomainFirst),
420 "srf" => Ok(MinionVariableOrder::SmallestRatioFirst),
421 "ldf" => Ok(MinionVariableOrder::LargestDomainFirst),
422 "random" => Ok(MinionVariableOrder::Random),
423 "conflict" => Ok(MinionVariableOrder::Conflict),
424 "wdeg" => Ok(MinionVariableOrder::WeightedDegree),
425 "domoverwdeg" => Ok(MinionVariableOrder::DomainOverWeightedDegree),
426 other => Err(format!(
427 "unknown minion variable order '{other}', expected one of: static, sdf, srf, ldf, \
428 random, conflict, wdeg, domoverwdeg"
429 )),
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn sat_backend_defaults_to_available_native_backend() {
439 let cli =
440 Cli::try_parse_from(["conjure-oxide", "solve", "model.essence", "--solver", "sat"])
441 .unwrap();
442 #[cfg(all(feature = "sat-cadical", not(target_family = "wasm")))]
443 assert_eq!(cli.global_args.sat_backend, SatBackend::Cadical);
444 #[cfg(not(all(feature = "sat-cadical", not(target_family = "wasm"))))]
445 assert_eq!(cli.global_args.sat_backend, SatBackend::Batsat);
446 }
447
448 #[test]
449 fn sat_backend_option_accepts_only_compiled_backends() {
450 for name in ["cadical", "batsat"] {
451 let result = Cli::try_parse_from([
452 "conjure-oxide",
453 "solve",
454 "model.essence",
455 "--solver",
456 "sat",
457 "--sat-backend",
458 name,
459 ]);
460 let available = SatBackend::value_variants()
461 .iter()
462 .find(|backend| backend.to_string() == name);
463 match available {
464 Some(backend) => assert_eq!(result.unwrap().global_args.sat_backend, *backend),
465 None => assert_eq!(
466 result.unwrap_err().kind(),
467 clap::error::ErrorKind::InvalidValue
468 ),
469 }
470 }
471 }
472
473 #[cfg(not(feature = "z3"))]
474 #[test]
475 fn selecting_z3_without_support_reports_the_missing_feature() {
476 let error =
477 Cli::try_parse_from(["conjure-oxide", "solve", "model.essence", "--solver", "z3"])
478 .unwrap_err();
479 assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation);
480 assert!(
481 error
482 .to_string()
483 .contains("Z3 solver support was not compiled in")
484 );
485 }
486
487 #[test]
489 fn version_flag_works_without_a_subcommand() {
490 for flag in ["--version", "-V"] {
491 let err = Cli::try_parse_from(["conjure-oxide", flag]).unwrap_err();
492 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
493 assert!(err.to_string().starts_with("conjure-oxide "));
494 }
495 }
496
497 #[test]
498 fn compact_is_the_default_cli_heuristic() {
499 let cli = Cli::try_parse_from(["conjure-oxide", "solve", "model.essence"]).unwrap();
500 assert_eq!(cli.global_args.heuristic, Heuristic::Compact);
501 }
502
503 #[test]
504 fn auto_is_the_default_comprehension_expander() {
505 let cli = Cli::try_parse_from(["conjure-oxide", "solve", "model.essence"]).unwrap();
506 assert_eq!(
507 cli.global_args.comprehension_expander,
508 QuantifiedExpander::Auto
509 );
510 }
511
512 #[test]
513 fn solver_seed_defaults_to_zero_and_can_be_overridden() {
514 let cli = Cli::try_parse_from(["conjure-oxide", "solve", "model.essence"]).unwrap();
515 assert_eq!(cli.global_args.solver_seed, 0);
516
517 let cli = Cli::try_parse_from([
518 "conjure-oxide",
519 "solve",
520 "model.essence",
521 "--solver-seed",
522 "42",
523 ])
524 .unwrap();
525 assert_eq!(cli.global_args.solver_seed, 42);
526 }
527
528 #[test]
529 fn parses_all_minion_variable_orders() {
530 let cases = [
531 ("static", MinionVariableOrder::Static),
532 ("sdf", MinionVariableOrder::SmallestDomainFirst),
533 ("srf", MinionVariableOrder::SmallestRatioFirst),
534 ("ldf", MinionVariableOrder::LargestDomainFirst),
535 ("random", MinionVariableOrder::Random),
536 ("conflict", MinionVariableOrder::Conflict),
537 ("wdeg", MinionVariableOrder::WeightedDegree),
538 ("domoverwdeg", MinionVariableOrder::DomainOverWeightedDegree),
539 ];
540
541 for (name, expected) in cases {
542 let cli = Cli::try_parse_from([
543 "conjure-oxide",
544 "solve",
545 "model.essence",
546 "--minion-varorder",
547 name,
548 ])
549 .unwrap();
550 assert_eq!(cli.global_args.minion_varorder, Some(expected));
551 }
552 }
553}