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
49fn flush_logging() {
51 if let Some(state) = LOGGING_STATE.get() {
52 state.flush();
53 }
54}
55
56pub(crate) fn exit(code: i32) -> ! {
58 flush_logging();
59 std::process::exit(code)
60}
61
62struct FlushLoggingOnDrop;
64
65impl Drop for FlushLoggingOnDrop {
66 fn drop(&mut self) {
67 flush_logging();
68 }
69}
70
71pub fn main() {
72 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 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
264fn 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}