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, DEFAULT_MINION_DISCRETE_THRESHOLD, Heuristic,
8 Parser as InputParser, QuantifiedExpander, Rewriter, SolverFamily,
9};
10use conjure_cp::solver::adaptors::{MinionValueOrder, MinionVariableOrder};
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");
17
18#[derive(Clone, Debug, Subcommand)]
20pub enum Command {
21 Solve(solve::Args),
23 PrintJsonSchema,
25 TestSolve(test_solve::Args),
30 Completion(CompletionArgs),
32 Pretty(pretty::Args),
33 ServerLSP,
35}
36
37#[derive(Clone, Debug, Parser)]
39#[command(
40 author,
41 about = "Conjure Oxide: Automated Constraints Modelling Toolkit",
42 before_help = "Full documentation can be found online at: https://conjure-cp.github.io/conjure-oxide",
43 disable_help_flag = true,
45 version = git_version!(),
46 disable_version_flag = true,
47 display_name = "conjure-oxide",
48 arg_required_else_help = false
51)]
52pub struct Cli {
53 #[command(subcommand)]
54 pub subcommand: Command,
55
56 #[command(flatten)]
57 pub global_args: GlobalArgs,
58
59 #[arg(long = "version", short = 'V', action = ArgAction::Version)]
63 pub version: (),
64}
65
66#[derive(Debug, Clone, Args)]
67pub struct GlobalArgs {
68 #[arg(long, action = clap::ArgAction::Help, global = true)]
70 pub help: (),
71
72 #[arg(long, value_name = "EXTRA_RULE_SETS", global = true)]
74 pub extra_rule_sets: Vec<String>,
75
76 #[arg(
80 long,
81 short = 'v',
82 action = ArgAction::Count,
83 global = true,
84 conflicts_with = "quiet",
85 help_heading = LOGGING_HELP_HEADING
86 )]
87 pub verbose: u8,
88
89 #[arg(long, short = 'q', global = true, help_heading = LOGGING_HELP_HEADING)]
91 pub quiet: bool,
92
93 #[arg(long, global = true, help_heading=LOGGING_HELP_HEADING)]
95 pub rule_trace: Option<PathBuf>,
96
97 #[arg(long, global = true, help_heading=LOGGING_HELP_HEADING)]
102 pub rule_trace_aggregates: Option<PathBuf>,
103
104 #[arg(long, default_value_t = false, global = true, help_heading=LOGGING_HELP_HEADING)]
109 pub rule_trace_cdp: bool,
110
111 #[arg(
115 long = "rule-attempt-trace",
116 global = true,
117 help_heading=LOGGING_HELP_HEADING
118 )]
119 pub rule_attempt_trace: Option<PathBuf>,
120
121 #[arg(
125 long,
126 default_value_t = InputParser::default(),
127 value_parser = parse_parser,
128 global = true,
129 help_heading = CONFIGURATION_HELP_HEADING
130 )]
131 pub parser: InputParser,
132
133 #[arg(long, default_value_t = Rewriter::default(), value_parser = parse_rewriter, global = true, help_heading = CONFIGURATION_HELP_HEADING)]
143 pub rewriter: Rewriter,
144
145 #[arg(
151 long,
152 default_value_t = QuantifiedExpander::Auto,
153 value_parser = parse_comprehension_expander,
154 global = true,
155 help_heading = CONFIGURATION_HELP_HEADING
156 )]
157 pub comprehension_expander: QuantifiedExpander,
158
159 #[arg(
167 long,
168 short = 'h',
169 default_value_t = Heuristic::Compact,
170 value_parser = parse_cli_heuristic,
171 global = true,
172 help_heading = CONFIGURATION_HELP_HEADING
173 )]
174 pub heuristic: Heuristic,
175
176 #[arg(
181 long,
182 value_name = "INTS",
183 value_delimiter = ',',
184 global = true,
185 help_heading = CONFIGURATION_HELP_HEADING
186 )]
187 pub responses: Vec<usize>,
188
189 #[arg(
191 long,
192 default_value_t = DEFAULT_HEURISTIC_SEED,
193 global = true,
194 help_heading = CONFIGURATION_HELP_HEADING
195 )]
196 pub seed: u64,
197
198 #[arg(
200 long,
201 default_value_t = 0,
202 global = true,
203 help_heading = CONFIGURATION_HELP_HEADING
204 )]
205 pub solver_seed: u32,
206
207 #[arg(
213 long,
214 default_value_t = Channelling::No,
215 value_parser = parse_cli_channelling,
216 global = true,
217 help_heading = CONFIGURATION_HELP_HEADING
218 )]
219 pub channelling: Channelling,
220
221 #[arg(
230 long,
231 value_name = "SOLVER",
232 value_parser = parse_solver_family,
233 default_value = "minion",
234 short = 's',
235 global = true,
236 help_heading = CONFIGURATION_HELP_HEADING
237 )]
238 pub solver: SolverFamily,
239
240 #[arg(
244 long,
245 default_value_t = DEFAULT_MINION_DISCRETE_THRESHOLD,
246 global = true,
247 help_heading = CONFIGURATION_HELP_HEADING
248 )]
249 pub minion_discrete_threshold: usize,
250
251 #[arg(
256 long,
257 value_name = "ORDER",
258 value_parser = parse_minion_variable_order,
259 global = true,
260 help_heading = CONFIGURATION_HELP_HEADING
261 )]
262 pub minion_varorder: Option<MinionVariableOrder>,
263
264 #[arg(
268 long,
269 value_name = "ORDER",
270 value_parser = parse_minion_value_order,
271 global = true,
272 help_heading = CONFIGURATION_HELP_HEADING
273 )]
274 pub minion_valorder: Option<MinionValueOrder>,
275
276 #[arg(long,global=true, value_names=["filename"], next_line_help=true, help_heading=LOGGING_HELP_HEADING)]
285 pub save_solver_input_file: Option<PathBuf>,
286
287 #[arg(long, global = true, help_heading = CONFIGURATION_HELP_HEADING)]
291 pub solver_timeout: Option<humantime::Duration>,
292
293 #[arg(long, value_name = "PATH", global = true, help_heading = LOGGING_HELP_HEADING)]
295 pub log_file: Option<PathBuf>,
296
297 #[arg(long, value_enum, requires = "log_file", global = true, help_heading = LOGGING_HELP_HEADING)]
299 pub log_format: Option<LogFormat>,
300
301 #[arg(long, value_enum, requires = "log_file", global = true, help_heading = LOGGING_HELP_HEADING)]
303 pub log_detail: Option<LogDetail>,
304}
305
306#[derive(Clone, Copy, Debug, Default, ValueEnum)]
307pub enum LogFormat {
308 #[default]
309 Text,
310 Json,
311}
312
313#[derive(Clone, Copy, Debug, Default, ValueEnum)]
314pub enum LogDetail {
315 #[default]
316 Stages,
317 Applications,
318 Attempts,
319}
320
321#[derive(Debug, Clone, Args)]
322pub struct CompletionArgs {
323 #[arg(value_enum)]
325 pub shell: Shell,
326}
327
328#[derive(Debug, Clone, Copy, clap::ValueEnum)]
329pub enum ShellTypes {
330 Bash,
331 Zsh,
332 Fish,
333 PowerShell,
334 Elvish,
335}
336
337fn parse_comprehension_expander(input: &str) -> Result<QuantifiedExpander, String> {
338 input.parse()
339}
340
341fn parse_cli_heuristic(input: &str) -> Result<Heuristic, String> {
342 match input.parse::<Heuristic>()? {
343 Heuristic::All => {
344 Err("heuristic 'x' (all) is not supported by the command line yet".to_string())
345 }
346 heuristic => Ok(heuristic),
347 }
348}
349
350fn parse_cli_channelling(input: &str) -> Result<Channelling, String> {
351 input.parse::<Channelling>()
352}
353
354fn parse_rewriter(input: &str) -> Result<Rewriter, String> {
355 input.parse::<Rewriter>()
356}
357
358fn parse_solver_family(input: &str) -> Result<SolverFamily, String> {
359 input.parse()
360}
361
362fn parse_parser(input: &str) -> Result<InputParser, String> {
363 input.parse()
364}
365
366fn parse_minion_value_order(input: &str) -> Result<MinionValueOrder, String> {
367 match input {
368 "ascend" => Ok(MinionValueOrder::Ascend),
369 "descend" => Ok(MinionValueOrder::Descend),
370 "random" => Ok(MinionValueOrder::Random),
371 other => Err(format!(
372 "unknown minion value order '{other}', expected one of: ascend, descend, random"
373 )),
374 }
375}
376
377fn parse_minion_variable_order(input: &str) -> Result<MinionVariableOrder, String> {
378 match input {
379 "static" => Ok(MinionVariableOrder::Static),
380 "sdf" => Ok(MinionVariableOrder::SmallestDomainFirst),
381 "srf" => Ok(MinionVariableOrder::SmallestRatioFirst),
382 "ldf" => Ok(MinionVariableOrder::LargestDomainFirst),
383 "random" => Ok(MinionVariableOrder::Random),
384 "conflict" => Ok(MinionVariableOrder::Conflict),
385 "wdeg" => Ok(MinionVariableOrder::WeightedDegree),
386 "domoverwdeg" => Ok(MinionVariableOrder::DomainOverWeightedDegree),
387 other => Err(format!(
388 "unknown minion variable order '{other}', expected one of: static, sdf, srf, ldf, \
389 random, conflict, wdeg, domoverwdeg"
390 )),
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
400 fn version_flag_works_without_a_subcommand() {
401 for flag in ["--version", "-V"] {
402 let err = Cli::try_parse_from(["conjure-oxide", flag]).unwrap_err();
403 assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
404 assert!(err.to_string().starts_with("conjure-oxide "));
405 }
406 }
407
408 #[test]
409 fn compact_is_the_default_cli_heuristic() {
410 let cli = Cli::try_parse_from(["conjure-oxide", "solve", "model.essence"]).unwrap();
411 assert_eq!(cli.global_args.heuristic, Heuristic::Compact);
412 }
413
414 #[test]
415 fn auto_is_the_default_comprehension_expander() {
416 let cli = Cli::try_parse_from(["conjure-oxide", "solve", "model.essence"]).unwrap();
417 assert_eq!(
418 cli.global_args.comprehension_expander,
419 QuantifiedExpander::Auto
420 );
421 }
422
423 #[test]
424 fn solver_seed_defaults_to_zero_and_can_be_overridden() {
425 let cli = Cli::try_parse_from(["conjure-oxide", "solve", "model.essence"]).unwrap();
426 assert_eq!(cli.global_args.solver_seed, 0);
427
428 let cli = Cli::try_parse_from([
429 "conjure-oxide",
430 "solve",
431 "model.essence",
432 "--solver-seed",
433 "42",
434 ])
435 .unwrap();
436 assert_eq!(cli.global_args.solver_seed, 42);
437 }
438
439 #[test]
440 fn parses_all_minion_variable_orders() {
441 let cases = [
442 ("static", MinionVariableOrder::Static),
443 ("sdf", MinionVariableOrder::SmallestDomainFirst),
444 ("srf", MinionVariableOrder::SmallestRatioFirst),
445 ("ldf", MinionVariableOrder::LargestDomainFirst),
446 ("random", MinionVariableOrder::Random),
447 ("conflict", MinionVariableOrder::Conflict),
448 ("wdeg", MinionVariableOrder::WeightedDegree),
449 ("domoverwdeg", MinionVariableOrder::DomainOverWeightedDegree),
450 ];
451
452 for (name, expected) in cases {
453 let cli = Cli::try_parse_from([
454 "conjure-oxide",
455 "solve",
456 "model.essence",
457 "--minion-varorder",
458 name,
459 ])
460 .unwrap();
461 assert_eq!(cli.global_args.minion_varorder, Some(expected));
462 }
463 }
464}