A reasonable configuration language rcl-lang.org
configuration-language json
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at master 457 lines 17 kB view raw
1// RCL -- A reasonable configuration language. 2// Copyright 2023 Ruud van Asseldonk 3 4// Licensed under the Apache License, Version 2.0 (the "License"); 5// you may not use this file except in compliance with the License. 6// A copy of the License has been included in the root of the repository. 7 8use std::io::{Stdout, Write}; 9use std::path::Path; 10 11use rcl::cli::{ 12 self, Cmd, EvalOptions, FormatTarget, GlobalOptions, OutputTarget, StyleOptions, Target, 13}; 14use rcl::error::{Error, Result}; 15use rcl::loader::{Loader, SandboxMode}; 16use rcl::markup::{MarkupMode, MarkupString}; 17use rcl::pprint::{self, Doc}; 18use rcl::runtime::{self, Value}; 19use rcl::source::{DocId, Inputs, Span}; 20use rcl::tracer::StderrTracer; 21use rcl::typecheck; 22 23/// The result of [`App::process_format_targets`]. 24struct FormatResult { 25 /// Whether we printed to stdout. 26 stdout: bool, 27 /// Whether we updated files in place. 28 in_place: bool, 29 /// Number of files changed, or that would change. 30 n_changed: u32, 31 /// Number of files inspected. 32 n_loaded: u32, 33} 34 35struct App { 36 loader: Loader, 37 opts: GlobalOptions, 38} 39 40impl App { 41 fn initialize_filesystem(&mut self, sandbox_mode: SandboxMode) -> Result<()> { 42 self.loader 43 .initialize_filesystem(sandbox_mode, self.opts.workdir.as_deref()) 44 } 45 46 fn print_string(&self, mode: MarkupMode, data: MarkupString, out: &mut dyn Write) { 47 let res = data.write_bytes(mode, out); 48 if res.is_err() { 49 // If we fail to print to stdout/stderr, there is no point in 50 // printing an error, just exit then. 51 std::process::exit(1); 52 } 53 } 54 55 /// Write a string to a file. 56 fn print_to_file_impl( 57 &self, 58 mode: MarkupMode, 59 data: MarkupString, 60 out_path: &Path, 61 ) -> std::io::Result<()> { 62 let f = std::fs::File::create(out_path)?; 63 let mut w = std::io::BufWriter::new(f); 64 data.write_bytes(mode, &mut w)?; 65 // Flush to force errors to materialize. Without this, flush would 66 // happen on drop, which has no opportunity to report errors. 67 w.flush() 68 } 69 70 /// Write a string to a file. 71 fn print_to_file(&self, mode: MarkupMode, data: MarkupString, out_path: &str) -> Result<()> { 72 let out_path = self.loader.resolve_cli_output_path(out_path); 73 74 self.print_to_file_impl(mode, data, out_path.as_ref()) 75 .map_err(|err| { 76 // The concat! macro is not exported, we'll make do with a vec here. 77 let parts = vec![ 78 "Failed to write to file '".into(), 79 Doc::path(out_path), 80 "': ".into(), 81 err.to_string().into(), 82 ]; 83 Error::new(Doc::Concat(parts)).into() 84 }) 85 } 86 87 fn get_markup_for_target(&self, target: &OutputTarget, stdout: &Stdout) -> MarkupMode { 88 match target { 89 OutputTarget::Stdout => self 90 .opts 91 .markup 92 .unwrap_or_else(|| MarkupMode::default_for_fd(stdout)), 93 // When the output is a file, we don't want to put ANSI escape codes 94 // in the file; --output is unaffected by --color. 95 OutputTarget::File(..) => MarkupMode::None, 96 } 97 } 98 99 fn print_doc_target( 100 &self, 101 output: OutputTarget, 102 style_opts: &StyleOptions, 103 doc: Doc, 104 ) -> Result<()> { 105 let stdout = std::io::stdout(); 106 let markup = self.get_markup_for_target(&output, &stdout); 107 let cfg = pprint::Config { 108 width: Some(style_opts.width), 109 }; 110 let result = doc.println(&cfg); 111 match output { 112 OutputTarget::Stdout => { 113 let mut out = stdout.lock(); 114 self.print_string(markup, result, &mut out); 115 } 116 OutputTarget::File(fname) => { 117 self.print_to_file(markup, result, &fname)?; 118 } 119 }; 120 Ok(()) 121 } 122 123 fn print_doc_stderr(&self, doc: Doc) { 124 let stderr = std::io::stderr(); 125 let markup = self 126 .opts 127 .markup 128 .unwrap_or_else(|| MarkupMode::default_for_fd(&stderr)); 129 let cfg = pprint::Config { width: Some(80) }; 130 let result = doc.println(&cfg); 131 let mut out = stderr.lock(); 132 self.print_string(markup, result, &mut out); 133 } 134 135 pub fn print_value( 136 &self, 137 eval_opts: &EvalOptions, 138 style_opts: &StyleOptions, 139 output: OutputTarget, 140 value_span: Span, 141 value: &Value, 142 ) -> Result<()> { 143 let out_doc = rcl::cmd_eval::format_value(eval_opts.format, value_span, value)?; 144 145 // Prepend the banner if the user specified one. 146 let out_doc = match eval_opts.banner.as_ref() { 147 Some(banner) => Doc::lines(banner) + Doc::HardBreak + out_doc, 148 None => out_doc, 149 }; 150 151 self.print_doc_target(output, style_opts, out_doc) 152 } 153 154 fn print_fatal_error(&self, err: Error) -> ! { 155 let inputs = self.loader.as_inputs(); 156 let err_doc = err.report(&inputs); 157 self.print_doc_stderr(err_doc); 158 // Regardless of whether printing to stderr failed or not, the error was 159 // fatal, so we exit with code 1. 160 std::process::exit(1); 161 } 162 163 fn get_tracer(&self) -> StderrTracer { 164 StderrTracer::new(self.opts.markup) 165 } 166 167 /// Load all targets, apply `apply_one` to each, and write each output if applicable. 168 fn process_format_targets<F>( 169 &mut self, 170 output: OutputTarget, 171 style_opts: StyleOptions, 172 targets: FormatTarget, 173 mut apply_one: F, 174 ) -> Result<FormatResult> 175 where 176 F: for<'a> FnMut(&'a Inputs, DocId, &'a mut rcl::cst::Expr) -> Result<Doc<'a>>, 177 { 178 let cfg = pprint::Config { 179 width: Some(style_opts.width), 180 }; 181 182 let mut result = FormatResult { 183 stdout: false, 184 in_place: false, 185 n_changed: 0, 186 n_loaded: 0, 187 }; 188 189 let fnames = match targets { 190 FormatTarget::Stdout { fname } => { 191 let doc = self.loader.load_cli_target(&fname)?; 192 let mut cst = self.loader.get_cst(doc)?; 193 let inputs = self.loader.as_inputs(); 194 let res = apply_one(&inputs, doc, &mut cst)?; 195 self.print_doc_target(output, &style_opts, res)?; 196 result.stdout = true; 197 return Ok(result); 198 } 199 FormatTarget::InPlace { fnames } => { 200 result.in_place = true; 201 fnames 202 } 203 FormatTarget::Check { mut fnames } => { 204 // For in-place formatting we really need files, but for checking, 205 // we can check stdin if the user did not specify any files. 206 if fnames.is_empty() { 207 fnames.push(Target::StdinDefault); 208 } 209 fnames 210 } 211 }; 212 213 for target in fnames { 214 result.n_loaded += 1; 215 let doc = self.loader.load_cli_target(&target)?; 216 let mut cst = self.loader.get_cst(doc)?; 217 let inputs = self.loader.as_inputs(); 218 let fmt_doc = apply_one(&inputs, doc, &mut cst)?; 219 let res = fmt_doc.println(&cfg); 220 let formatted = res.to_string_no_markup(); 221 let did_change = self.loader.get_doc(doc).data != &formatted[..]; 222 223 if result.in_place { 224 let fname = match target { 225 Target::File(fname) => fname, 226 Target::Stdin => { 227 let msg = 228 "Rewriting in-place is only possible for named files, not for stdin."; 229 return Error::new(msg).err(); 230 } 231 Target::StdinDefault => { 232 unreachable!("In-place default is empty list, not stdin.") 233 } 234 }; 235 // We only write to the file if we changed anything. This ensures 236 // that we don't cause rebuilds for build systems that look at mtimes, 237 // that we don't waste space on CoW filesystems, and that we don't 238 // unnecessarily burn through SSDs in general. 239 if did_change { 240 result.n_changed += 1; 241 self.print_to_file(MarkupMode::None, res, &fname)?; 242 } 243 } else { 244 // We are in the --check case, not the --in-place case. 245 if did_change { 246 result.n_changed += 1; 247 println!("Would modify {}", self.loader.get_doc(doc).name); 248 } 249 } 250 } 251 252 Ok(result) 253 } 254 255 fn main(&mut self) -> Result<()> { 256 let (opts, cmd) = cli::parse(std::env::args().collect())?; 257 self.opts = opts; 258 259 match cmd { 260 Cmd::Help { usage } => { 261 println!("{}", usage.concat().trim()); 262 std::process::exit(0) 263 } 264 265 Cmd::Build { 266 eval_opts, 267 build_mode, 268 fname, 269 } => { 270 // Evaluation options support a depfile, but this is not implemented 271 // for builds, we'd have to put multiple output filenames in there 272 // and that is not supported right now. 273 if eval_opts.output_depfile.is_some() { 274 return Error::new("Generating depfiles is not supported for 'rcl build'.") 275 .err(); 276 } 277 278 self.initialize_filesystem(eval_opts.sandbox)?; 279 280 // TODO: We can make these members, then we can share a lot of code between commands! 281 let mut tracer = self.get_tracer(); 282 let mut type_env = typecheck::prelude(); 283 let mut value_env = runtime::prelude(); 284 let doc = self.loader.load_cli_target(&fname)?; 285 286 // TODO: Would be nice to be able to feed in an expected type. 287 let val = self 288 .loader 289 .evaluate(&mut type_env, &mut value_env, doc, &mut tracer)?; 290 291 let full_span = self.loader.get_span(doc); 292 293 rcl::cmd_build::execute_build(&self.loader, build_mode, doc, full_span, val) 294 } 295 296 Cmd::Evaluate { 297 eval_opts, 298 style_opts, 299 fname, 300 output, 301 } => { 302 self.initialize_filesystem(eval_opts.sandbox)?; 303 304 let mut tracer = self.get_tracer(); 305 let mut type_env = typecheck::prelude(); 306 let mut value_env = runtime::prelude(); 307 let doc = self.loader.load_cli_target(&fname)?; 308 let val = self 309 .loader 310 .evaluate(&mut type_env, &mut value_env, doc, &mut tracer)?; 311 312 if let Some(depfile_path) = eval_opts.output_depfile.as_ref() { 313 self.loader.write_depfile(&output, depfile_path)?; 314 } 315 316 let body_span = self.loader.get_span(doc); 317 self.print_value(&eval_opts, &style_opts, output, body_span, &val) 318 } 319 320 Cmd::Query { 321 eval_opts, 322 style_opts, 323 fname, 324 query: expr, 325 output, 326 } => { 327 self.initialize_filesystem(eval_opts.sandbox)?; 328 329 let input = self.loader.load_cli_target(&fname)?; 330 let query = self.loader.load_string("query", expr); 331 332 // First we evaluate the input document. 333 let mut tracer = self.get_tracer(); 334 let mut type_env = typecheck::prelude(); 335 let mut value_env = runtime::prelude(); 336 let val_input = 337 self.loader 338 .evaluate(&mut type_env, &mut value_env, input, &mut tracer)?; 339 340 // Then we bind that to the variable `input`, and in that context, 341 // we evaluate the query expression. The environments should be 342 // clean at this point, so we can reuse them. 343 type_env.push("input".into(), typecheck::type_any().clone()); 344 value_env.push("input".into(), val_input); 345 let val_result = 346 self.loader 347 .evaluate(&mut type_env, &mut value_env, query, &mut tracer)?; 348 349 if let Some(depfile_path) = eval_opts.output_depfile.as_ref() { 350 self.loader.write_depfile(&output, depfile_path)?; 351 } 352 353 let body_span = self.loader.get_span(query); 354 self.print_value(&eval_opts, &style_opts, output, body_span, &val_result) 355 } 356 357 Cmd::Format { 358 style_opts, 359 target, 360 output, 361 } => { 362 // Unrestricted is safe, because `format` does not evaluate documents. 363 self.initialize_filesystem(SandboxMode::Unrestricted)?; 364 let stats = self.process_format_targets( 365 output, 366 style_opts, 367 target, 368 |inputs, _doc, cst| Ok(rcl::fmt_cst::format_expr(inputs, cst)), 369 )?; 370 match (stats.n_changed, stats.n_loaded) { 371 (_, _) if stats.stdout => { /* No stats to print, we printed the doc. */ } 372 (k, n) if stats.in_place => println!("Reformatted {k} of {n} files."), 373 (0, 1) => println!("The file is formatted correctly."), 374 (0, n) => println!("All {n} files are formatted correctly."), 375 _ => { 376 let parts = vec![ 377 stats.n_changed.to_string().into(), 378 Doc::str(" of "), 379 stats.n_loaded.to_string().into(), 380 Doc::str(" files would be reformatted."), 381 ]; 382 return Error::new(Doc::Concat(parts)).err(); 383 } 384 } 385 Ok(()) 386 } 387 388 Cmd::Patch { 389 style_opts, 390 target, 391 output, 392 path, 393 replacement, 394 } => { 395 // Unrestricted is safe, because `patch` does not evaluate documents. 396 self.initialize_filesystem(SandboxMode::Unrestricted)?; 397 398 let mut patcher = Some(rcl::patch::Patcher::new( 399 &mut self.loader, 400 &path, 401 replacement, 402 )?); 403 404 let stats = self.process_format_targets( 405 output, 406 style_opts, 407 target, 408 |inputs, doc, input_cst| { 409 let input_doc = &inputs[doc]; 410 let patcher = patcher.take().ok_or_else(|| { 411 Error::new("'rcl patch' handles only one target file.") 412 })?; 413 patcher.apply(input_doc.data, input_doc.span, input_cst)?; 414 Ok(rcl::fmt_cst::format_expr(inputs, input_cst)) 415 }, 416 )?; 417 match (stats.n_changed, stats.n_loaded) { 418 (_, _) if stats.stdout => { /* No stats to print, we printed the doc. */ } 419 (1, 1) if stats.in_place => println!("Patch applied successfully."), 420 (0, 1) => println!("The patch is a no-op."), 421 (1, 1) => return Error::new("File would be patched or reformatted.").err(), 422 _ => unreachable!("Patch patches at most 1 document."), 423 } 424 Ok(()) 425 } 426 427 Cmd::Highlight { fname } => { 428 // Unrestricted is safe, because `highlight` does not evaluate documents. 429 self.initialize_filesystem(SandboxMode::Unrestricted)?; 430 let doc = self.loader.load_cli_target(&fname)?; 431 let tokens = self.loader.get_tokens(doc)?; 432 let data = self.loader.get_doc(doc).data; 433 let result = rcl::highlight::highlight(&tokens, data); 434 let out = std::io::stdout(); 435 let markup = self.get_markup_for_target(&OutputTarget::Stdout, &out); 436 self.print_string(markup, result, &mut out.lock()); 437 Ok(()) 438 } 439 440 Cmd::Version => { 441 println!("RCL version {}", env!("CARGO_PKG_VERSION")); 442 Ok(()) 443 } 444 } 445 } 446} 447 448fn main() { 449 let mut app = App { 450 opts: GlobalOptions::default(), 451 loader: Loader::new(), 452 }; 453 454 if let Err(err) = app.main() { 455 app.print_fatal_error(*err); 456 } 457}