1
use std::path::PathBuf;
2

            
3
use clap::{Args, Parser, Subcommand};
4

            
5
use clap_complete::Shell;
6
use conjure_cp::settings::{
7
    DEFAULT_MINION_DISCRETE_THRESHOLD, Parser as InputParser, QuantifiedExpander, Rewriter,
8
    SolverFamily,
9
};
10
use conjure_cp::solver::adaptors::MinionValueOrder;
11

            
12
use crate::{pretty, solve, test_solve};
13

            
14
pub(crate) const DEBUG_HELP_HEADING: Option<&str> = Some("Debug");
15
pub(crate) const LOGGING_HELP_HEADING: Option<&str> = Some("Logging & Output");
16
pub(crate) const CONFIGURATION_HELP_HEADING: Option<&str> = Some("Configuration");
17
pub(crate) const OPTIMISATIONS_HELP_HEADING: Option<&str> = Some("Optimisations");
18

            
19
/// All subcommands of conjure-oxide
20
#[derive(Clone, Debug, Subcommand)]
21
pub enum Command {
22
    /// Solve a model
23
    Solve(solve::Args),
24
    /// Print the JSON info file schema
25
    PrintJsonSchema,
26
    /// Tests whether the Essence model is solvable with Conjure Oxide, and whether it gets the
27
    /// same solutions as Conjure.
28
    ///
29
    /// Return-code will be 0 if the solutions match, 1 if they don't, and >1 on crash.
30
    TestSolve(test_solve::Args),
31
    /// Generate a completion script for the shell provided
32
    Completion(CompletionArgs),
33
    Pretty(pretty::Args),
34
    // Run the language server
35
    ServerLSP,
36
}
37

            
38
/// Global command line arguments.
39
#[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
)]
45
pub struct Cli {
46
    #[command(subcommand)]
47
    pub subcommand: Command,
48

            
49
    #[command(flatten)]
50
    pub global_args: GlobalArgs,
51

            
52
    /// Print version
53
    #[arg(long = "version", short = 'V')]
54
    pub version: bool,
55
}
56

            
57
#[derive(Debug, Clone, Args)]
58
pub struct GlobalArgs {
59
    /// Extra rule sets to enable
60
    #[arg(long, value_name = "EXTRA_RULE_SETS", global = true)]
61
    pub extra_rule_sets: Vec<String>,
62

            
63
    /// Log verbosely
64
    #[arg(long, short = 'v', help = "Log verbosely to stderr", global = true, help_heading = LOGGING_HELP_HEADING)]
65
    pub verbose: bool,
66

            
67
    // --no-x flag disables --x flag : https://jwodder.github.io/kbits/posts/clap-bool-negate/
68
    /// Check for multiple equally applicable rules, exiting if any are found.
69
    ///
70
    /// Only compatible with the default rewriter.
71
    #[arg(
72
        long,
73
        overrides_with = "_no_check_equally_applicable_rules",
74
        default_value_t = false,
75
        global = true,
76
        help_heading= DEBUG_HELP_HEADING
77
    )]
78
    pub check_equally_applicable_rules: bool,
79

            
80
    /// Output file for the default rule trace.
81
    #[arg(long, global = true, help_heading=LOGGING_HELP_HEADING)]
82
    pub rule_trace: Option<PathBuf>,
83

            
84
    /// Output file for aggregated rule-application counts.
85
    ///
86
    /// The file is updated incrementally in the format:
87
    /// `total_rule_applications: N`, followed by one line per rule.
88
    #[arg(long, global = true, help_heading=LOGGING_HELP_HEADING)]
89
    pub rule_trace_aggregates: Option<PathBuf>,
90

            
91
    /// Continue rule trace generation during solver-time CDP rewrites.
92
    ///
93
    /// This is off by default, so follow-up dominance-blocking rewrites do not contribute to the
94
    /// trace.
95
    #[arg(long, default_value_t = false, global = true, help_heading=LOGGING_HELP_HEADING)]
96
    pub rule_trace_cdp: bool,
97

            
98
    /// Output file for verbose rule-attempt trace in CSV format.
99
    ///
100
    /// Each row includes: elapsed_s, rule_level, rule_name, rule_set, status, expression.
101
    #[arg(long, global = true, help_heading=LOGGING_HELP_HEADING)]
102
    pub rule_trace_verbose: Option<PathBuf>,
103

            
104
    /// Do not check for multiple equally applicable rules [default].
105
    ///
106
    /// Only compatible with the default rewriter.
107
    #[arg(long, global = true, help_heading = DEBUG_HELP_HEADING)]
108
    pub _no_check_equally_applicable_rules: bool,
109

            
110
    /// Which parser to use.
111
    ///
112
    /// Possible values: `tree-sitter`, `via-conjure`.
113
    #[arg(
114
        long,
115
        default_value_t = InputParser::ViaConjure,
116
        value_parser = parse_parser,
117
        global = true,
118
        help_heading = CONFIGURATION_HELP_HEADING
119
    )]
120
    pub parser: InputParser,
121

            
122
    /// Which rewriter to use.
123
    ///
124
    /// Possible values: `naive`, `morph`
125
    #[arg(long, default_value_t = Rewriter::Naive, value_parser = parse_rewriter, global = true, help_heading = CONFIGURATION_HELP_HEADING)]
126
    pub rewriter: Rewriter,
127

            
128
    /// Which strategy to use for expanding quantified variables in comprehensions.
129
    ///
130
    /// Possible values: `native`, `via-solver`, `via-solver-ac`.
131
    #[arg(
132
        long,
133
        default_value_t = QuantifiedExpander::Native,
134
        value_parser = parse_comprehension_expander,
135
        global = true,
136
        help_heading = CONFIGURATION_HELP_HEADING
137
    )]
138
    pub comprehension_expander: QuantifiedExpander,
139

            
140
    /// Solver family to use.
141
    ///
142
    /// Possible values: `minion`, `sat`, `sat-log`, `sat-direct`, `sat-order`,
143
    /// `smt[-<ints>][-<matrices>][-nodiscrete]`
144
    /// where `<ints>` is `lia` or `bv`, and `<matrices>` is `arrays` or `atomic`.
145
    #[arg(
146
        long,
147
        value_name = "SOLVER",
148
        value_parser = parse_solver_family,
149
        default_value = "minion",
150
        short = 's',
151
        global = true,
152
        help_heading = CONFIGURATION_HELP_HEADING
153
    )]
154
    pub solver: SolverFamily,
155

            
156
    /// Int-domain size threshold for using Minion `DISCRETE` variables.
157
    ///
158
    /// If an int domain has size <= this value, Conjure Oxide emits `DISCRETE`; otherwise `BOUND`.
159
    #[arg(
160
        long,
161
        default_value_t = DEFAULT_MINION_DISCRETE_THRESHOLD,
162
        global = true,
163
        help_heading = CONFIGURATION_HELP_HEADING
164
    )]
165
    pub minion_discrete_threshold: usize,
166

            
167
    /// Override Minion value ordering.
168
    ///
169
    /// Possible values: `ascend`, `descend`, `random`.
170
    #[arg(
171
        long,
172
        value_name = "ORDER",
173
        value_parser = parse_minion_value_order,
174
        global = true,
175
        help_heading = CONFIGURATION_HELP_HEADING
176
    )]
177
    pub minion_valorder: Option<MinionValueOrder>,
178

            
179
    /// Save a solver input file to <filename>.
180
    ///
181
    /// This input file will be in a format compatible by the command-line
182
    /// interface of the selected solver. For example, when the solver is Minion,
183
    /// a valid .minion file will be output.
184
    ///
185
    /// This file is for informational purposes only; the results of running
186
    /// this file cannot be used by Conjure Oxide in any way.
187
    #[arg(long,global=true, value_names=["filename"], next_line_help=true, help_heading=LOGGING_HELP_HEADING)]
188
    pub save_solver_input_file: Option<PathBuf>,
189

            
190
    /// Stop the solver after the given timeout.
191
    ///
192
    /// Currently only SMT supports this feature.
193
    #[arg(long, global = true, help_heading = OPTIMISATIONS_HELP_HEADING)]
194
    pub solver_timeout: Option<humantime::Duration>,
195

            
196
    /// Generate log files
197
    #[arg(long, default_value_t = false, global = true, help_heading = LOGGING_HELP_HEADING)]
198
    pub log: bool,
199

            
200
    /// Output file for conjure-oxide's text logs
201
    #[arg(long, value_name = "LOGFILE", global = true, help_heading = LOGGING_HELP_HEADING)]
202
    pub logfile: Option<PathBuf>,
203

            
204
    /// Output file for conjure-oxide's json logs
205
    #[arg(long, value_name = "JSON LOGFILE", global = true, help_heading = LOGGING_HELP_HEADING)]
206
    pub logfile_json: Option<PathBuf>,
207
}
208

            
209
#[derive(Debug, Clone, Args)]
210
pub struct CompletionArgs {
211
    /// Shell type for which to generate the completion script
212
    #[arg(value_enum)]
213
    pub shell: Shell,
214
}
215

            
216
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
217
pub enum ShellTypes {
218
    Bash,
219
    Zsh,
220
    Fish,
221
    PowerShell,
222
    Elvish,
223
}
224

            
225
360
fn parse_comprehension_expander(input: &str) -> Result<QuantifiedExpander, String> {
226
360
    input.parse()
227
360
}
228

            
229
360
fn parse_rewriter(input: &str) -> Result<Rewriter, String> {
230
360
    input.parse::<Rewriter>()
231
360
}
232

            
233
360
fn parse_solver_family(input: &str) -> Result<SolverFamily, String> {
234
360
    input.parse()
235
360
}
236

            
237
360
fn parse_parser(input: &str) -> Result<InputParser, String> {
238
360
    input.parse()
239
360
}
240

            
241
fn parse_minion_value_order(input: &str) -> Result<MinionValueOrder, String> {
242
    match input {
243
        "ascend" => Ok(MinionValueOrder::Ascend),
244
        "descend" => Ok(MinionValueOrder::Descend),
245
        "random" => Ok(MinionValueOrder::Random),
246
        other => Err(format!(
247
            "unknown minion value order '{other}', expected one of: ascend, descend, random"
248
        )),
249
    }
250
}