1
#![allow(clippy::unwrap_used)]
2

            
3
#[cfg(feature = "mimalloc")]
4
#[global_allocator]
5
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
6

            
7
mod cli;
8
mod pretty;
9
mod print_info_schema;
10
mod rule_trace_aggregates;
11
mod solve;
12
mod test_solve;
13
use clap::{CommandFactory, Parser};
14
use clap_complete::generate;
15
use cli::{Cli, GlobalArgs, LogDetail, LogFormat};
16
use pretty::run_pretty_command;
17
use print_info_schema::run_print_info_schema_command;
18
use rule_trace_aggregates::RuleTraceAggregatesHandle;
19
use solve::run_solve_command;
20
use std::fs::File;
21
use std::io;
22
use std::sync::{Arc, OnceLock};
23
use test_solve::run_test_solve_command;
24

            
25
use conjure_cp_rules as _;
26

            
27
use tracing_subscriber::filter::{FilterFn, LevelFilter};
28
use tracing_subscriber::layer::SubscriberExt as _;
29
use tracing_subscriber::util::SubscriberInitExt as _;
30
use tracing_subscriber::{EnvFilter, Layer, fmt};
31

            
32
#[cfg(feature = "lsp")]
33
use conjure_cp_lsp::server;
34

            
35
struct LoggingState {
36
    rule_trace_aggregates: Option<RuleTraceAggregatesHandle>,
37
}
38

            
39
impl LoggingState {
40
626
    fn flush(&self) {
41
626
        if let Some(handle) = &self.rule_trace_aggregates {
42
36
            handle.flush();
43
590
        }
44
626
    }
45
}
46

            
47
static LOGGING_STATE: OnceLock<LoggingState> = OnceLock::new();
48

            
49
/// Writes buffered log outputs, such as the final rule-trace aggregates.
50
626
fn flush_logging() {
51
626
    if let Some(state) = LOGGING_STATE.get() {
52
626
        state.flush();
53
    }
54
626
}
55

            
56
/// Flushes buffered log outputs, then exits. Use instead of [`std::process::exit`].
57
314
pub(crate) fn exit(code: i32) -> ! {
58
314
    flush_logging();
59
314
    std::process::exit(code)
60
}
61

            
62
/// Flushes on return and while unwinding from a panic.
63
struct FlushLoggingOnDrop;
64

            
65
impl Drop for FlushLoggingOnDrop {
66
312
    fn drop(&mut self) {
67
312
        flush_logging();
68
312
    }
69
}
70

            
71
318
pub fn main() {
72
    // exit with 2 instead of 1 on failure,like grep
73
318
    match run() {
74
        Ok(_) => {
75
282
            exit(0);
76
        }
77
        Err(e) => {
78
36
            eprintln!("{e:?}");
79
36
            exit(2);
80
        }
81
    }
82
}
83

            
84
318
pub fn run() -> anyhow::Result<()> {
85
318
    let cli = Cli::parse();
86

            
87
318
    let logging_state = setup_logging(&cli.global_args)?;
88
318
    let _ = LOGGING_STATE.set(logging_state);
89
318
    let _flush = FlushLoggingOnDrop;
90
318
    run_subcommand(cli)
91
318
}
92

            
93
314
fn setup_logging(global_args: &GlobalArgs) -> anyhow::Result<LoggingState> {
94
    // It consists of composable layers, each of which logs to a different place in a different
95
    // format.
96
314
    let default_stderr_level = match (global_args.quiet, global_args.verbose) {
97
        (true, _) => LevelFilter::OFF,
98
314
        (false, 0) => LevelFilter::WARN,
99
        (false, 1) => LevelFilter::INFO,
100
        (false, 2) => LevelFilter::DEBUG,
101
        (false, _) => LevelFilter::TRACE,
102
    };
103

            
104
314
    let env_filter = if global_args.quiet || global_args.verbose > 0 {
105
        EnvFilter::new(default_stderr_level.to_string())
106
    } else {
107
314
        EnvFilter::builder()
108
314
            .with_default_directive(default_stderr_level.into())
109
314
            .from_env_lossy()
110
    };
111

            
112
314
    let stderr_layer = if global_args.verbose > 0 {
113
        Layer::boxed(
114
            tracing_subscriber::fmt::layer()
115
                .pretty()
116
                .with_writer(Arc::new(std::io::stderr()))
117
                .with_ansi(true)
118
                .with_filter(env_filter)
119
                .with_filter(general_log_filter()),
120
        )
121
    } else {
122
314
        Layer::boxed(
123
314
            tracing_subscriber::fmt::layer()
124
314
                .compact()
125
314
                .with_writer(Arc::new(std::io::stderr()))
126
314
                .with_ansi(true)
127
314
                .with_filter(env_filter)
128
314
                .with_filter(general_log_filter()),
129
314
        )
130
    };
131

            
132
314
    let rule_trace_layer = global_args.rule_trace.clone().map(|x| {
133
18
        let file = File::create(x).expect("Unable to create rule trace file");
134
18
        fmt::layer()
135
18
            .with_writer(file)
136
18
            .with_level(false)
137
18
            .without_time()
138
18
            .with_target(false)
139
18
            .with_filter(EnvFilter::new("rule_engine_rule_trace=trace"))
140
41054
            .with_filter(FilterFn::new(|meta| {
141
41054
                meta.target() == "rule_engine_rule_trace"
142
41054
            }))
143
18
    });
144

            
145
314
    let rule_attempt_trace_layer = global_args.rule_attempt_trace.clone().map(|x| {
146
4
        let file = File::create(x).expect("Unable to create rule attempt trace file");
147
4
        fmt::layer()
148
4
            .with_writer(file)
149
4
            .with_level(false)
150
4
            .without_time()
151
4
            .with_target(false)
152
4
            .compact()
153
4
            .with_ansi(false)
154
4
            .with_filter(EnvFilter::new("rule_engine_rule_attempt_trace=trace"))
155
1441636
            .with_filter(FilterFn::new(|meta| {
156
1441636
                meta.target() == "rule_engine_rule_attempt_trace"
157
1441636
            }))
158
4
    });
159

            
160
314
    let rule_trace_aggregates_handle = global_args
161
314
        .rule_trace_aggregates
162
314
        .clone()
163
314
        .map(RuleTraceAggregatesHandle::new)
164
314
        .transpose()?;
165

            
166
314
    let rule_trace_aggregates_layer = rule_trace_aggregates_handle.as_ref().map(|handle| {
167
18
        handle
168
18
            .layer()
169
18
            .with_filter(EnvFilter::new("rule_engine_rule_trace_aggregates=trace"))
170
1514692
            .with_filter(FilterFn::new(|meta| {
171
1514692
                meta.target() == "rule_engine_rule_trace_aggregates"
172
1514692
            }))
173
18
    });
174

            
175
314
    let log_format = global_args.log_format.unwrap_or_default();
176
314
    let log_detail = global_args.log_detail.unwrap_or_default();
177
314
    let text_file = match (log_format, global_args.log_file.as_ref()) {
178
        (LogFormat::Text, Some(path)) => Some(
179
4
            File::options()
180
4
                .truncate(true)
181
4
                .write(true)
182
4
                .create(true)
183
4
                .append(false)
184
4
                .open(path)?,
185
        ),
186
310
        _ => None,
187
    };
188

            
189
314
    let file_level = match log_detail {
190
312
        LogDetail::Stages => LevelFilter::INFO,
191
        LogDetail::Applications => LevelFilter::DEBUG,
192
2
        LogDetail::Attempts => LevelFilter::TRACE,
193
    };
194
314
    let text_file_layer = text_file.map(|file| {
195
4
        tracing_subscriber::fmt::layer()
196
4
            .compact()
197
4
            .with_ansi(false)
198
4
            .with_writer(Arc::new(file))
199
4
            .with_filter(file_level)
200
4
            .with_filter(general_log_filter())
201
4
    });
202
314
    let json_file = match (log_format, global_args.log_file.as_ref()) {
203
        (LogFormat::Json, Some(path)) => Some(
204
2
            File::options()
205
2
                .truncate(true)
206
2
                .write(true)
207
2
                .create(true)
208
2
                .append(false)
209
2
                .open(path)?,
210
        ),
211
312
        _ => None,
212
    };
213
314
    let json_file_layer = json_file.map(|file| {
214
2
        tracing_subscriber::fmt::layer()
215
2
            .json()
216
2
            .with_writer(Arc::new(file))
217
2
            .with_filter(file_level)
218
2
            .with_filter(general_log_filter())
219
2
    });
220

            
221
314
    tracing_subscriber::registry()
222
314
        .with(stderr_layer)
223
314
        .with(rule_trace_layer)
224
314
        .with(rule_attempt_trace_layer)
225
314
        .with(rule_trace_aggregates_layer)
226
314
        .with(text_file_layer)
227
314
        .with(json_file_layer)
228
314
        .init();
229

            
230
314
    Ok(LoggingState {
231
314
        rule_trace_aggregates: rule_trace_aggregates_handle,
232
314
    })
233
314
}
234

            
235
320
fn general_log_filter() -> FilterFn<fn(&tracing::Metadata<'_>) -> bool> {
236
3574366
    fn is_general_log(meta: &tracing::Metadata<'_>) -> bool {
237
3574366
        !meta.target().starts_with("rule_engine_rule_")
238
3574366
    }
239

            
240
320
    FilterFn::new(is_general_log)
241
320
}
242

            
243
fn run_completion_command(completion_args: cli::CompletionArgs) -> anyhow::Result<()> {
244
    let mut cmd = Cli::command();
245
    let shell = completion_args.shell;
246
    let name = cmd.get_name().to_string();
247

            
248
    eprintln!("Generating completion for {shell}...");
249

            
250
    generate(shell, &mut cmd, name, &mut io::stdout());
251
    Ok(())
252
}
253

            
254
fn run_lsp_server() -> anyhow::Result<()> {
255
    #[cfg(feature = "lsp")]
256
    {
257
        server::main();
258
        Ok(())
259
    }
260
    #[cfg(not(feature = "lsp"))]
261
    anyhow::bail!("Language server support was not compiled in (enable the `lsp` feature).");
262
}
263

            
264
/// Runs the selected subcommand
265
314
fn run_subcommand(cli: Cli) -> anyhow::Result<()> {
266
314
    let global_args = cli.global_args;
267
314
    match cli.subcommand {
268
182
        cli::Command::Solve(solve_args) => run_solve_command(global_args, solve_args),
269
4
        cli::Command::TestSolve(local_args) => run_test_solve_command(global_args, local_args),
270
        cli::Command::PrintJsonSchema => run_print_info_schema_command(),
271
        cli::Command::Completion(completion_args) => run_completion_command(completion_args),
272
128
        cli::Command::Pretty(pretty_args) => run_pretty_command(global_args, pretty_args),
273
        cli::Command::ServerLSP => run_lsp_server(),
274
    }
275
314
}
276

            
277
#[cfg(test)]
278
mod tests {
279
    use conjure_cp::parse::conjure_json::{get_example_model, get_example_model_by_path};
280

            
281
    #[cfg(not(feature = "lsp"))]
282
    #[test]
283
    fn language_server_without_support_reports_the_missing_feature() {
284
        let error = super::run_lsp_server().unwrap_err();
285
        assert!(
286
            error
287
                .to_string()
288
                .contains("Language server support was not compiled in")
289
        );
290
    }
291

            
292
    #[test]
293
1
    fn test_get_example_model_success() {
294
1
        let filename = "input";
295
1
        get_example_model(filename).unwrap();
296
1
    }
297

            
298
    #[test]
299
1
    fn test_get_example_model_by_filepath() {
300
1
        let filepath = "../../test-suite/tests/exhaustive/basic/misc/xyz/input.essence";
301
1
        get_example_model_by_path(filepath).unwrap();
302
1
    }
303

            
304
    #[test]
305
1
    fn test_get_example_model_fail_empty_filename() {
306
1
        let filename = "";
307
1
        get_example_model(filename).unwrap_err();
308
1
    }
309

            
310
    #[test]
311
1
    fn test_get_example_model_fail_empty_filepath() {
312
1
        let filepath = "";
313
1
        get_example_model_by_path(filepath).unwrap_err();
314
1
    }
315
}