Skip to main content

conjure_oxide/
main.rs

1#![allow(clippy::unwrap_used)]
2
3#[cfg(feature = "mimalloc")]
4#[global_allocator]
5static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
6
7mod cli;
8mod pretty;
9mod print_info_schema;
10mod rule_trace_aggregates;
11mod solve;
12mod test_solve;
13use clap::{CommandFactory, Parser};
14use clap_complete::generate;
15use cli::{Cli, GlobalArgs, LogDetail, LogFormat};
16use pretty::run_pretty_command;
17use print_info_schema::run_print_info_schema_command;
18use rule_trace_aggregates::RuleTraceAggregatesHandle;
19use solve::run_solve_command;
20use std::fs::File;
21use std::io;
22use std::sync::{Arc, OnceLock};
23use test_solve::run_test_solve_command;
24
25use conjure_cp_rules as _;
26
27use tracing_subscriber::filter::{FilterFn, LevelFilter};
28use tracing_subscriber::layer::SubscriberExt as _;
29use tracing_subscriber::util::SubscriberInitExt as _;
30use tracing_subscriber::{EnvFilter, Layer, fmt};
31
32#[cfg(feature = "lsp")]
33use conjure_cp_lsp::server;
34
35struct LoggingState {
36    rule_trace_aggregates: Option<RuleTraceAggregatesHandle>,
37}
38
39impl LoggingState {
40    fn flush(&self) {
41        if let Some(handle) = &self.rule_trace_aggregates {
42            handle.flush();
43        }
44    }
45}
46
47static LOGGING_STATE: OnceLock<LoggingState> = OnceLock::new();
48
49/// Writes buffered log outputs, such as the final rule-trace aggregates.
50fn flush_logging() {
51    if let Some(state) = LOGGING_STATE.get() {
52        state.flush();
53    }
54}
55
56/// Flushes buffered log outputs, then exits. Use instead of [`std::process::exit`].
57pub(crate) fn exit(code: i32) -> ! {
58    flush_logging();
59    std::process::exit(code)
60}
61
62/// Flushes on return and while unwinding from a panic.
63struct FlushLoggingOnDrop;
64
65impl Drop for FlushLoggingOnDrop {
66    fn drop(&mut self) {
67        flush_logging();
68    }
69}
70
71pub fn main() {
72    // exit with 2 instead of 1 on failure,like grep
73    match run() {
74        Ok(_) => {
75            exit(0);
76        }
77        Err(e) => {
78            eprintln!("{e:?}");
79            exit(2);
80        }
81    }
82}
83
84pub fn run() -> anyhow::Result<()> {
85    let cli = Cli::parse();
86
87    let logging_state = setup_logging(&cli.global_args)?;
88    let _ = LOGGING_STATE.set(logging_state);
89    let _flush = FlushLoggingOnDrop;
90    run_subcommand(cli)
91}
92
93fn 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    let default_stderr_level = match (global_args.quiet, global_args.verbose) {
97        (true, _) => LevelFilter::OFF,
98        (false, 0) => LevelFilter::WARN,
99        (false, 1) => LevelFilter::INFO,
100        (false, 2) => LevelFilter::DEBUG,
101        (false, _) => LevelFilter::TRACE,
102    };
103
104    let env_filter = if global_args.quiet || global_args.verbose > 0 {
105        EnvFilter::new(default_stderr_level.to_string())
106    } else {
107        EnvFilter::builder()
108            .with_default_directive(default_stderr_level.into())
109            .from_env_lossy()
110    };
111
112    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        Layer::boxed(
123            tracing_subscriber::fmt::layer()
124                .compact()
125                .with_writer(Arc::new(std::io::stderr()))
126                .with_ansi(true)
127                .with_filter(env_filter)
128                .with_filter(general_log_filter()),
129        )
130    };
131
132    let rule_trace_layer = global_args.rule_trace.clone().map(|x| {
133        let file = File::create(x).expect("Unable to create rule trace file");
134        fmt::layer()
135            .with_writer(file)
136            .with_level(false)
137            .without_time()
138            .with_target(false)
139            .with_filter(EnvFilter::new("rule_engine_rule_trace=trace"))
140            .with_filter(FilterFn::new(|meta| {
141                meta.target() == "rule_engine_rule_trace"
142            }))
143    });
144
145    let rule_attempt_trace_layer = global_args.rule_attempt_trace.clone().map(|x| {
146        let file = File::create(x).expect("Unable to create rule attempt trace file");
147        fmt::layer()
148            .with_writer(file)
149            .with_level(false)
150            .without_time()
151            .with_target(false)
152            .compact()
153            .with_ansi(false)
154            .with_filter(EnvFilter::new("rule_engine_rule_attempt_trace=trace"))
155            .with_filter(FilterFn::new(|meta| {
156                meta.target() == "rule_engine_rule_attempt_trace"
157            }))
158    });
159
160    let rule_trace_aggregates_handle = global_args
161        .rule_trace_aggregates
162        .clone()
163        .map(RuleTraceAggregatesHandle::new)
164        .transpose()?;
165
166    let rule_trace_aggregates_layer = rule_trace_aggregates_handle.as_ref().map(|handle| {
167        handle
168            .layer()
169            .with_filter(EnvFilter::new("rule_engine_rule_trace_aggregates=trace"))
170            .with_filter(FilterFn::new(|meta| {
171                meta.target() == "rule_engine_rule_trace_aggregates"
172            }))
173    });
174
175    let log_format = global_args.log_format.unwrap_or_default();
176    let log_detail = global_args.log_detail.unwrap_or_default();
177    let text_file = match (log_format, global_args.log_file.as_ref()) {
178        (LogFormat::Text, Some(path)) => Some(
179            File::options()
180                .truncate(true)
181                .write(true)
182                .create(true)
183                .append(false)
184                .open(path)?,
185        ),
186        _ => None,
187    };
188
189    let file_level = match log_detail {
190        LogDetail::Stages => LevelFilter::INFO,
191        LogDetail::Applications => LevelFilter::DEBUG,
192        LogDetail::Attempts => LevelFilter::TRACE,
193    };
194    let text_file_layer = text_file.map(|file| {
195        tracing_subscriber::fmt::layer()
196            .compact()
197            .with_ansi(false)
198            .with_writer(Arc::new(file))
199            .with_filter(file_level)
200            .with_filter(general_log_filter())
201    });
202    let json_file = match (log_format, global_args.log_file.as_ref()) {
203        (LogFormat::Json, Some(path)) => Some(
204            File::options()
205                .truncate(true)
206                .write(true)
207                .create(true)
208                .append(false)
209                .open(path)?,
210        ),
211        _ => None,
212    };
213    let json_file_layer = json_file.map(|file| {
214        tracing_subscriber::fmt::layer()
215            .json()
216            .with_writer(Arc::new(file))
217            .with_filter(file_level)
218            .with_filter(general_log_filter())
219    });
220
221    tracing_subscriber::registry()
222        .with(stderr_layer)
223        .with(rule_trace_layer)
224        .with(rule_attempt_trace_layer)
225        .with(rule_trace_aggregates_layer)
226        .with(text_file_layer)
227        .with(json_file_layer)
228        .init();
229
230    Ok(LoggingState {
231        rule_trace_aggregates: rule_trace_aggregates_handle,
232    })
233}
234
235fn general_log_filter() -> FilterFn<fn(&tracing::Metadata<'_>) -> bool> {
236    fn is_general_log(meta: &tracing::Metadata<'_>) -> bool {
237        !meta.target().starts_with("rule_engine_rule_")
238    }
239
240    FilterFn::new(is_general_log)
241}
242
243fn 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
254fn 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
265fn run_subcommand(cli: Cli) -> anyhow::Result<()> {
266    let global_args = cli.global_args;
267    match cli.subcommand {
268        cli::Command::Solve(solve_args) => run_solve_command(global_args, solve_args),
269        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        cli::Command::Pretty(pretty_args) => run_pretty_command(global_args, pretty_args),
273        cli::Command::ServerLSP => run_lsp_server(),
274    }
275}
276
277#[cfg(test)]
278mod 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    fn test_get_example_model_success() {
294        let filename = "input";
295        get_example_model(filename).unwrap();
296    }
297
298    #[test]
299    fn test_get_example_model_by_filepath() {
300        let filepath = "../../test-suite/tests/exhaustive/basic/misc/xyz/input.essence";
301        get_example_model_by_path(filepath).unwrap();
302    }
303
304    #[test]
305    fn test_get_example_model_fail_empty_filename() {
306        let filename = "";
307        get_example_model(filename).unwrap_err();
308    }
309
310    #[test]
311    fn test_get_example_model_fail_empty_filepath() {
312        let filepath = "";
313        get_example_model_by_path(filepath).unwrap_err();
314    }
315}