1
use std::fs::File;
2
use std::io::Write;
3
use std::path::PathBuf;
4
use std::process::exit;
5
use std::sync::Arc;
6

            
7
use anyhow::Result as AnyhowResult;
8
use anyhow::{anyhow, bail};
9
use clap::{arg, command, Parser};
10
use conjure_core::rule_engine::rewrite_naive;
11
use conjure_core::Model;
12
use conjure_oxide::defaults::get_default_rule_sets;
13
use schemars::schema_for;
14
use serde_json::to_string_pretty;
15

            
16
use conjure_core::context::Context;
17
use conjure_oxide::find_conjure::conjure_executable;
18
use conjure_oxide::model_from_json;
19
use conjure_oxide::rule_engine::{
20
    get_rule_priorities, get_rules_vec, resolve_rule_sets, rewrite_model,
21
};
22

            
23
use conjure_oxide::utils::conjure::{get_minion_solutions, minion_solutions_to_json};
24
use conjure_oxide::SolverFamily;
25
use tracing_subscriber::filter::LevelFilter;
26
use tracing_subscriber::layer::SubscriberExt as _;
27
use tracing_subscriber::util::SubscriberInitExt as _;
28
use tracing_subscriber::{EnvFilter, Layer};
29

            
30
static AFTER_HELP_TEXT: &str = include_str!("help_text.txt");
31

            
32
#[derive(Parser)]
33
#[command(author, version, about, long_about = None, after_long_help=AFTER_HELP_TEXT)]
34
struct Cli {
35
    #[arg(value_name = "INPUT_ESSENCE", help = "The input Essence file")]
36
    input_file: PathBuf,
37

            
38
    #[arg(
39
        long,
40
        value_name = "EXTRA_RULE_SETS",
41
        help = "Names of extra rule sets to enable"
42
    )]
43
    extra_rule_sets: Vec<String>,
44

            
45
    #[arg(
46
        long,
47
        value_enum,
48
        value_name = "SOLVER",
49
        short = 's',
50
        help = "Solver family use (Minion by default)"
51
    )]
52
    solver: Option<SolverFamily>, // ToDo this should probably set the solver adapter
53

            
54
    #[arg(
55
        long,
56
        default_value_t = 0,
57
        short = 'n',
58
        help = "number of solutions to return (0 for all)"
59
    )]
60
    number_of_solutions: i32,
61
    // TODO: subcommands instead of these being a flag.
62
    #[arg(
63
        long,
64
        default_value_t = false,
65
        help = "Print the schema for the info JSON and exit"
66
    )]
67
    print_info_schema: bool,
68

            
69
    #[arg(long, help = "Save execution info as JSON to the given file-path.")]
70
    info_json_path: Option<PathBuf>,
71

            
72
    #[arg(
73
        long,
74
        help = "use the, in development, dirty-clean optimising rewriter",
75
        default_value_t = false
76
    )]
77
    use_optimising_rewriter: bool,
78

            
79
    #[arg(
80
        long,
81
        short = 'o',
82
        help = "Save solutions to a JSON file (prints to stdout by default)"
83
    )]
84
    output: Option<PathBuf>,
85

            
86
    #[arg(long, short = 'v', help = "Log verbosely to sterr")]
87
    verbose: bool,
88

            
89
    /// Do not run the solver.
90
    ///
91
    /// The rewritten model is printed to stdout in an Essence-style syntax (but is not necessarily
92
    /// valid Essence).
93
    #[arg(long, default_value_t = false)]
94
    no_run_solver: bool,
95

            
96
    // --no-x flag disables --x flag : https://jwodder.github.io/kbits/posts/clap-bool-negate/
97
    /// Check for multiple equally applicable rules, exiting if any are found.
98
    ///
99
    /// Only compatible with the default rewriter.
100
    #[arg(
101
        long,
102
        overrides_with = "_no_check_equally_applicable_rules",
103
        default_value_t = false
104
    )]
105
    check_equally_applicable_rules: bool,
106

            
107
    /// Do not check for multiple equally applicable rules [default].
108
    ///
109
    /// Only compatible with the default rewriter.
110
    #[arg(long)]
111
    _no_check_equally_applicable_rules: bool,
112
}
113

            
114
#[allow(clippy::unwrap_used)]
115
pub fn main() -> AnyhowResult<()> {
116
    let cli = Cli::parse();
117

            
118
    #[allow(clippy::unwrap_used)]
119
    if cli.print_info_schema {
120
        let schema = schema_for!(Context);
121
        println!("{}", serde_json::to_string_pretty(&schema).unwrap());
122
        return Ok(());
123
    }
124

            
125
    let target_family = cli.solver.unwrap_or(SolverFamily::Minion);
126
    let mut extra_rule_sets: Vec<String> = get_default_rule_sets();
127
    extra_rule_sets.extend(cli.extra_rule_sets.clone());
128

            
129
    // Logging:
130
    //
131
    // Using `tracing` framework, but this automatically reads stuff from `log`.
132
    //
133
    // A Subscriber is responsible for logging.
134
    //
135
    // It consists of composable layers, each of which logs to a different place in a different
136
    // format.
137
    let json_log_file = File::options()
138
        .create(true)
139
        .append(true)
140
        .open("conjure_oxide_log.json")?;
141

            
142
    let log_file = File::options()
143
        .create(true)
144
        .append(true)
145
        .open("conjure_oxide.log")?;
146

            
147
    // get log level from env-var RUST_LOG
148

            
149
    let json_layer = tracing_subscriber::fmt::layer()
150
        .json()
151
        .with_writer(Arc::new(json_log_file))
152
        .with_filter(LevelFilter::TRACE);
153

            
154
    let file_layer = tracing_subscriber::fmt::layer()
155
        .compact()
156
        .with_ansi(false)
157
        .with_writer(Arc::new(log_file))
158
        .with_filter(LevelFilter::TRACE);
159

            
160
    let default_stderr_level = if cli.verbose {
161
        LevelFilter::DEBUG
162
    } else {
163
        LevelFilter::WARN
164
    };
165

            
166
    let env_filter = EnvFilter::builder()
167
        .with_default_directive(default_stderr_level.into())
168
        .from_env_lossy();
169

            
170
    let stderr_layer = if cli.verbose {
171
        Layer::boxed(
172
            tracing_subscriber::fmt::layer()
173
                .pretty()
174
                .with_writer(Arc::new(std::io::stderr()))
175
                .with_ansi(true)
176
                .with_filter(env_filter),
177
        )
178
    } else {
179
        Layer::boxed(
180
            tracing_subscriber::fmt::layer()
181
                .compact()
182
                .with_writer(Arc::new(std::io::stderr()))
183
                .with_ansi(true)
184
                .with_filter(env_filter),
185
        )
186
    };
187

            
188
    // load the loggers
189
    tracing_subscriber::registry()
190
        .with(json_layer)
191
        .with(stderr_layer)
192
        .with(file_layer)
193
        .init();
194

            
195
    if target_family != SolverFamily::Minion {
196
        tracing::error!("Only the Minion solver is currently supported!");
197
        exit(1);
198
    }
199

            
200
    let rule_sets = match resolve_rule_sets(target_family, &extra_rule_sets) {
201
        Ok(rs) => rs,
202
        Err(e) => {
203
            tracing::error!("Error resolving rule sets: {}", e);
204
            exit(1);
205
        }
206
    };
207

            
208
    let pretty_rule_sets = rule_sets
209
        .iter()
210
        .map(|rule_set| rule_set.name)
211
        .collect::<Vec<_>>()
212
        .join(", ");
213

            
214
    tracing::info!("Enabled rule sets: [{}]", pretty_rule_sets);
215
    tracing::info!(
216
        target: "file",
217
        "Rule sets: {}",
218
        pretty_rule_sets
219
    );
220

            
221
    let rule_priorities = get_rule_priorities(&rule_sets)?;
222
    let rules_vec = get_rules_vec(&rule_priorities);
223

            
224
    tracing::info!(target: "file",
225
         "Rules and priorities: {}",
226
         rules_vec.iter()
227
            .map(|rule| format!("{}: {}", rule.name, rule_priorities.get(rule).unwrap_or(&0)))
228
            .collect::<Vec<_>>()
229
            .join(", "));
230

            
231
    tracing::info!(target: "file", "Input file: {}", cli.input_file.display());
232
    let input_file: &str = cli.input_file.to_str().ok_or(anyhow!(
233
        "Given input_file could not be converted to a string"
234
    ))?;
235

            
236
    /******************************************************/
237
    /*        Parse essence to json using Conjure         */
238
    /******************************************************/
239

            
240
    conjure_executable()
241
        .map_err(|e| anyhow!("Could not find correct conjure executable: {}", e))?;
242

            
243
    let mut cmd = std::process::Command::new("conjure");
244
    let output = cmd
245
        .arg("pretty")
246
        .arg("--output-format=astjson")
247
        .arg(input_file)
248
        .output()?;
249

            
250
    let conjure_stderr = String::from_utf8(output.stderr)?;
251
    if !conjure_stderr.is_empty() {
252
        bail!(conjure_stderr);
253
    }
254

            
255
    let astjson = String::from_utf8(output.stdout)?;
256

            
257
    let context = Context::new_ptr(
258
        target_family,
259
        extra_rule_sets.clone(),
260
        rules_vec.clone(),
261
        rule_sets.clone(),
262
    );
263

            
264
    context.write().unwrap().file_name = Some(cli.input_file.to_str().expect("").into());
265

            
266
    if cfg!(feature = "extra-rule-checks") {
267
        tracing::info!("extra-rule-checks: enabled");
268
    } else {
269
        tracing::info!("extra-rule-checks: disabled");
270
    }
271

            
272
    let mut model = model_from_json(&astjson, context.clone())?;
273

            
274
    tracing::info!("Initial model: \n{}\n", model);
275

            
276
    tracing::info!("Rewriting model...");
277

            
278
    if cli.use_optimising_rewriter {
279
        tracing::info!("Using the dirty-clean rewriter...");
280
        model = rewrite_model(&model, &rule_sets)?;
281
    } else {
282
        tracing::info!("Rewriting model...");
283
        model = rewrite_naive(&model, &rule_sets, cli.check_equally_applicable_rules)?;
284
    }
285

            
286
    tracing::info!("Rewritten model: \n{}\n", model);
287

            
288
    if cli.no_run_solver {
289
        println!("{}", model);
290
    } else {
291
        run_solver(&cli, model)?;
292
    }
293

            
294
    // still do postamble even if we didn't run the solver
295
    if let Some(path) = cli.info_json_path {
296
        #[allow(clippy::unwrap_used)]
297
        let context_obj = context.read().unwrap().clone();
298
        let generated_json = &serde_json::to_value(context_obj)?;
299
        let pretty_json = serde_json::to_string_pretty(&generated_json)?;
300
        File::create(path)?.write_all(pretty_json.as_bytes())?;
301
    }
302
    Ok(())
303
}
304

            
305
/// Runs the solver
306
fn run_solver(cli: &Cli, model: Model) -> anyhow::Result<()> {
307
    let out_file: Option<File> = match &cli.output {
308
        None => None,
309
        Some(pth) => Some(
310
            File::options()
311
                .create(true)
312
                .truncate(true)
313
                .write(true)
314
                .open(pth)?,
315
        ),
316
    };
317

            
318
    let solutions = get_minion_solutions(model, cli.number_of_solutions)?; // ToDo we need to properly set the solver adaptor here, not hard code minion
319
    tracing::info!(target: "file", "Solutions: {}", minion_solutions_to_json(&solutions));
320

            
321
    let solutions_json = minion_solutions_to_json(&solutions);
322
    let solutions_str = to_string_pretty(&solutions_json)?;
323
    match out_file {
324
        None => {
325
            println!("Solutions:");
326
            println!("{}", solutions_str);
327
        }
328
        Some(mut outf) => {
329
            outf.write_all(solutions_str.as_bytes())?;
330
            println!(
331
                "Solutions saved to {:?}",
332
                &cli.output.clone().unwrap().canonicalize()?
333
            )
334
        }
335
    }
336
    Ok(())
337
}
338

            
339
#[cfg(test)]
340
mod tests {
341
    use conjure_oxide::{get_example_model, get_example_model_by_path};
342

            
343
    #[test]
344
1
    fn test_get_example_model_success() {
345
1
        let filename = "input";
346
1
        get_example_model(filename).unwrap();
347
1
    }
348

            
349
    #[test]
350
1
    fn test_get_example_model_by_filepath() {
351
1
        let filepath = "tests/integration/xyz/input.essence";
352
1
        get_example_model_by_path(filepath).unwrap();
353
1
    }
354

            
355
    #[test]
356
1
    fn test_get_example_model_fail_empty_filename() {
357
1
        let filename = "";
358
1
        get_example_model(filename).unwrap_err();
359
1
    }
360

            
361
    #[test]
362
1
    fn test_get_example_model_fail_empty_filepath() {
363
1
        let filepath = "";
364
1
        get_example_model_by_path(filepath).unwrap_err();
365
1
    }
366
}