1
//! conjure_oxide solve sub-command
2
#![allow(clippy::unwrap_used)]
3
#[cfg(feature = "smt")]
4
use std::time::Duration;
5
use std::{
6
    fs::File,
7
    io::Write as _,
8
    path::PathBuf,
9
    process::exit,
10
    sync::{Arc, RwLock},
11
};
12

            
13
use anyhow::{anyhow, ensure};
14
use clap::ValueHint;
15
use conjure_cp::defaults::DEFAULT_RULE_SETS;
16
use conjure_cp::{
17
    Model,
18
    context::Context,
19
    rule_engine::{resolve_rule_sets, rewrite_morph, rewrite_naive},
20
    settings::{
21
        Rewriter, set_comprehension_expander, set_current_parser, set_current_rewriter,
22
        set_current_solver_family,
23
    },
24
    solver::Solver,
25
};
26
use conjure_cp::{
27
    parse::conjure_json::model_from_json, rule_engine::get_rules, settings::SolverFamily,
28
};
29
use conjure_cp::{parse::tree_sitter::parse_essence_file_native, solver::adaptors::*};
30
use conjure_cp_cli::find_conjure::conjure_executable;
31
use conjure_cp_cli::utils::conjure::{get_solutions, solutions_to_json};
32
use serde_json::to_string_pretty;
33

            
34
use crate::cli::{GlobalArgs, LOGGING_HELP_HEADING};
35

            
36
#[derive(Clone, Debug, clap::Args)]
37
pub struct Args {
38
    /// The input Essence file
39
    #[arg(value_name = "INPUT_ESSENCE", value_hint = ValueHint::FilePath)]
40
    pub input_file: PathBuf,
41

            
42
    /// Save execution info as JSON to the given filepath.
43
    #[arg(long ,value_hint=ValueHint::FilePath,help_heading=LOGGING_HELP_HEADING)]
44
    pub info_json_path: Option<PathBuf>,
45

            
46
    /// Do not run the solver.
47
    ///
48
    /// The rewritten model is printed to stdout in an Essence-style syntax
49
    /// (but is not necessarily valid Essence).
50
    #[arg(long, default_value_t = false)]
51
    pub no_run_solver: bool,
52

            
53
    /// Number of solutions to return. 0 returns all solutions
54
    #[arg(long, default_value_t = 0, short = 'n')]
55
    pub number_of_solutions: i32,
56

            
57
    /// Save solutions to the given JSON file
58
    #[arg(long, short = 'o', value_hint = ValueHint::FilePath,help_heading=LOGGING_HELP_HEADING)]
59
    pub output: Option<PathBuf>,
60
}
61

            
62
21
pub fn run_solve_command(global_args: GlobalArgs, solve_args: Args) -> anyhow::Result<()> {
63
21
    let input_file = solve_args.input_file.clone();
64

            
65
    // each step is in its own method so that similar commands
66
    // (e.g. testsolve) can reuse some of these steps.
67

            
68
21
    let context = init_context(&global_args, input_file)?;
69
21
    let model = parse(&global_args, Arc::clone(&context))?;
70
15
    let rewritten_model = rewrite(model, &global_args, Arc::clone(&context))?;
71
15
    let solver = init_solver(&global_args);
72

            
73
15
    if solve_args.no_run_solver {
74
        println!("{}", &rewritten_model);
75

            
76
        if let Some(path) = global_args.save_solver_input_file {
77
            let solver = solver.load_model(rewritten_model)?;
78
            eprintln!("Writing solver input file to {}", path.display());
79
            let mut file: Box<dyn std::io::Write> = Box::new(File::create(path)?);
80
            solver.write_solver_input_file(&mut file)?;
81
        }
82
    } else {
83
15
        run_solver(solver, &global_args, &solve_args, rewritten_model)?
84
    }
85

            
86
    // still do postamble even if we didn't run the solver
87
15
    if let Some(ref path) = solve_args.info_json_path {
88
        let context_obj = context.read().unwrap().clone();
89
        let generated_json = &serde_json::to_value(context_obj)?;
90
        let pretty_json = serde_json::to_string_pretty(&generated_json)?;
91
        File::create(path)?.write_all(pretty_json.as_bytes())?;
92
15
    }
93
15
    Ok(())
94
21
}
95

            
96
/// Returns a new Context and Solver for solving.
97
24
pub(crate) fn init_context(
98
24
    global_args: &GlobalArgs,
99
24
    input_file: PathBuf,
100
24
) -> anyhow::Result<Arc<RwLock<Context<'static>>>> {
101
24
    set_current_parser(global_args.parser);
102
24
    set_current_rewriter(global_args.rewriter);
103
24
    set_comprehension_expander(global_args.comprehension_expander);
104
24
    set_current_solver_family(global_args.solver);
105

            
106
24
    let target_family = global_args.solver;
107
24
    let mut extra_rule_sets: Vec<&str> = DEFAULT_RULE_SETS.to_vec();
108
24
    for rs in &global_args.extra_rule_sets {
109
        extra_rule_sets.push(rs.as_str());
110
    }
111

            
112
24
    if let SolverFamily::Sat(sat_encoding) = target_family {
113
        extra_rule_sets.push(sat_encoding.as_rule_set());
114
24
    }
115

            
116
24
    let rule_sets = match resolve_rule_sets(target_family, &extra_rule_sets) {
117
24
        Ok(rs) => rs,
118
        Err(e) => {
119
            tracing::error!("Error resolving rule sets: {}", e);
120
            exit(1);
121
        }
122
    };
123

            
124
24
    let pretty_rule_sets = rule_sets
125
24
        .iter()
126
24
        .map(|rule_set| rule_set.name)
127
24
        .collect::<Vec<_>>()
128
24
        .join(", ");
129

            
130
24
    tracing::info!("Enabled rule sets: [{}]", pretty_rule_sets);
131
24
    tracing::info!(
132
        target: "file",
133
        "Rule sets: {}",
134
        pretty_rule_sets
135
    );
136

            
137
24
    let rules = get_rules(&rule_sets)?.into_iter().collect::<Vec<_>>();
138
24
    tracing::info!(
139
        target: "file",
140
        "Rules: {}",
141
846
        rules.iter().map(|rd| format!("{rd}")).collect::<Vec<_>>().join("\n")
142
    );
143
24
    let context = Context::new_ptr(
144
24
        target_family,
145
72
        extra_rule_sets.iter().map(|rs| rs.to_string()).collect(),
146
24
        rules,
147
24
        rule_sets.clone(),
148
    );
149

            
150
24
    context.write().unwrap().file_name = Some(input_file.to_str().expect("").into());
151

            
152
24
    Ok(context)
153
24
}
154

            
155
15
pub(crate) fn init_solver(global_args: &GlobalArgs) -> Solver {
156
15
    let family = global_args.solver;
157
    #[cfg(feature = "smt")]
158
15
    let timeout_ms = global_args
159
15
        .solver_timeout
160
15
        .map(|dur| Duration::from(dur).as_millis())
161
15
        .map(|timeout_ms| u64::try_from(timeout_ms).expect("Timeout too large"));
162

            
163
15
    match family {
164
15
        SolverFamily::Minion => Solver::new(Minion::default()),
165
        SolverFamily::Sat(_) => Solver::new(Sat::default()),
166
        #[cfg(feature = "smt")]
167
        SolverFamily::Smt(theory_cfg) => Solver::new(Smt::new(timeout_ms, theory_cfg)),
168
    }
169
15
}
170

            
171
24
pub(crate) fn parse(
172
24
    global_args: &GlobalArgs,
173
24
    context: Arc<RwLock<Context<'static>>>,
174
24
) -> anyhow::Result<Model> {
175
24
    let input_file: String = context
176
24
        .read()
177
24
        .unwrap()
178
24
        .file_name
179
24
        .clone()
180
24
        .expect("context should contain the input file");
181

            
182
24
    tracing::info!(target: "file", "Input file: {}", input_file);
183
24
    match global_args.parser {
184
        conjure_cp::settings::Parser::TreeSitter => {
185
21
            parse_essence_file_native(input_file.as_str(), context.clone()).map_err(|e| e.into())
186
        }
187
        conjure_cp::settings::Parser::ViaConjure => {
188
3
            conjure_executable()
189
3
                .map_err(|e| anyhow!("Could not find correct conjure executable: {e}"))?;
190

            
191
3
            let mut cmd = std::process::Command::new("conjure");
192
3
            let output = cmd
193
3
                .arg("pretty")
194
3
                .arg("--output-format=astjson")
195
3
                .arg(input_file)
196
3
                .output()?;
197

            
198
3
            let conjure_stderr = String::from_utf8(output.stderr)?;
199

            
200
3
            ensure!(conjure_stderr.is_empty(), conjure_stderr);
201

            
202
3
            let astjson = String::from_utf8(output.stdout)?;
203

            
204
3
            if cfg!(feature = "extra-rule-checks") {
205
3
                tracing::info!("extra-rule-checks: enabled");
206
            } else {
207
                tracing::info!("extra-rule-checks: disabled");
208
            }
209

            
210
3
            model_from_json(&astjson, context.clone()).map_err(|e| anyhow!(e))
211
        }
212
    }
213
24
}
214

            
215
15
pub(crate) fn rewrite(
216
15
    model: Model,
217
15
    global_args: &GlobalArgs,
218
15
    context: Arc<RwLock<Context<'static>>>,
219
15
) -> anyhow::Result<Model> {
220
15
    tracing::info!("Initial model: \n{}\n", model);
221

            
222
15
    set_current_rewriter(global_args.rewriter);
223

            
224
15
    let comprehension_expander = global_args.comprehension_expander;
225
15
    set_comprehension_expander(comprehension_expander);
226
15
    tracing::info!("Comprehension expander: {}", comprehension_expander);
227

            
228
15
    let rule_sets = context.read().unwrap().rule_sets.clone();
229

            
230
15
    let new_model = match global_args.rewriter {
231
        Rewriter::Morph => {
232
            tracing::info!("Rewriting the model using the morph rewriter");
233
            rewrite_morph(
234
                model,
235
                &rule_sets,
236
                global_args.check_equally_applicable_rules,
237
            )
238
        }
239
        Rewriter::Naive => {
240
15
            tracing::info!("Rewriting the model using the default / naive rewriter");
241
15
            rewrite_naive(
242
15
                &model,
243
15
                &rule_sets,
244
15
                global_args.check_equally_applicable_rules,
245
            )?
246
        }
247
    };
248

            
249
15
    tracing::info!("Rewritten model: \n{}\n", new_model);
250
15
    Ok(new_model)
251
15
}
252

            
253
15
fn run_solver(
254
15
    solver: Solver,
255
15
    global_args: &GlobalArgs,
256
15
    cmd_args: &Args,
257
15
    model: Model,
258
15
) -> anyhow::Result<()> {
259
15
    let out_file: Option<File> = match &cmd_args.output {
260
15
        None => None,
261
        Some(pth) => Some(
262
            File::options()
263
                .create(true)
264
                .truncate(true)
265
                .write(true)
266
                .open(pth)?,
267
        ),
268
    };
269

            
270
15
    let solutions = get_solutions(
271
15
        solver,
272
15
        model,
273
15
        cmd_args.number_of_solutions,
274
15
        &global_args.save_solver_input_file,
275
    )?;
276
15
    tracing::info!(target: "file", "Solutions: {}", solutions_to_json(&solutions));
277

            
278
15
    let solutions_json = solutions_to_json(&solutions);
279
15
    let solutions_str = to_string_pretty(&solutions_json)?;
280
15
    match out_file {
281
15
        None => {
282
15
            println!("Solutions:");
283
15
            println!("{solutions_str}");
284
15
        }
285
        Some(mut outf) => {
286
            outf.write_all(solutions_str.as_bytes())?;
287
            println!(
288
                "Solutions saved to {:?}",
289
                &cmd_args.output.clone().unwrap().canonicalize()?
290
            )
291
        }
292
    }
293
15
    Ok(())
294
15
}