rune/compile/options.rs
1use core::fmt;
2
3use rust_alloc::boxed::Box;
4
5use crate::docstring;
6
7/// Error raised when trying to parse an invalid option.
8#[derive(Debug, Clone)]
9pub struct ParseOptionError {
10 env: Option<&'static str>,
11 option: Box<str>,
12}
13
14impl fmt::Display for ParseOptionError {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 write!(f, "Unsupported compile option `{}`", self.option)?;
17
18 if let Some(env) = self.env {
19 write!(f, " (environment `{env}`)")?;
20 }
21
22 Ok(())
23 }
24}
25
26impl core::error::Error for ParseOptionError {}
27
28/// The indentation to use when formatting.
29#[derive(Debug, Clone, Copy)]
30pub(crate) enum IndentStyle {
31 /// Indent using the given number of spaces.
32 #[cfg_attr(not(feature = "fmt"), allow(dead_code))]
33 Spaces(usize),
34 /// Indent using tabs.
35 Tabs,
36}
37
38/// Options specific to formatting.
39#[derive(Debug, Clone)]
40pub(crate) struct FmtOptions {
41 /// Attempt to format even when faced with syntax errors.
42 pub(crate) error_recovery: bool,
43 /// Force newline at end of document.
44 pub(crate) force_newline: bool,
45 /// The indentation to use.
46 pub(crate) indent: IndentStyle,
47}
48
49impl FmtOptions {
50 /// The default format option.
51 pub(crate) const DEFAULT: Self = Self {
52 error_recovery: false,
53 force_newline: true,
54 indent: IndentStyle::Spaces(4),
55 };
56
57 /// Parse an option with the extra diagnostics metadata.
58 ///
59 /// The `option` is the full option being parsed, and is used for error
60 /// reporting, while `head` and `tail` are the already split option key and
61 /// optional value.
62 fn parse_option_with(
63 &mut self,
64 option: &str,
65 head: &str,
66 tail: Option<&str>,
67 env: Option<&'static str>,
68 ) -> Result<(), ParseOptionError> {
69 match head {
70 "error-recovery" => {
71 self.error_recovery = tail.is_none_or(|s| s == "true");
72 }
73 "force-newline" => {
74 self.force_newline = tail.is_none_or(|s| s == "true");
75 }
76 "indent" => {
77 self.indent = match tail {
78 Some("tab") => IndentStyle::Tabs,
79 Some(n) => match n.parse() {
80 Ok(n) => IndentStyle::Spaces(n),
81 Err(..) => {
82 return Err(ParseOptionError {
83 env,
84 option: option.into(),
85 });
86 }
87 },
88 None => {
89 return Err(ParseOptionError {
90 env,
91 option: option.into(),
92 });
93 }
94 };
95 }
96 _ => {
97 return Err(ParseOptionError {
98 env,
99 option: option.into(),
100 });
101 }
102 }
103
104 Ok(())
105 }
106}
107
108impl Default for FmtOptions {
109 #[inline]
110 fn default() -> Self {
111 FmtOptions::DEFAULT
112 }
113}
114
115/// Documentation for a single compiler option.
116#[non_exhaustive]
117pub struct OptionMeta {
118 /// The key.
119 pub key: &'static str,
120 /// Whether the option is unstable or not.
121 pub unstable: bool,
122 /// The documentation for the option.
123 pub doc: &'static [&'static str],
124 /// The default value for the option.
125 pub default: &'static str,
126 /// Available options.
127 pub options: &'static str,
128}
129
130/// Options that can be provided to the compiler.
131///
132/// See [Build::with_options][crate::Build::with_options].
133#[derive(Debug, Clone)]
134pub struct Options {
135 /// Perform link-time checks.
136 pub(crate) link_checks: bool,
137 /// Memoize the instance function in a loop.
138 pub(crate) memoize_instance_fn: bool,
139 /// Include debug information when compiling.
140 pub(crate) debug_info: bool,
141 /// Support macros.
142 pub(crate) macros: bool,
143 /// Support bytecode caching.
144 pub(crate) bytecode: bool,
145 /// Build sources as scripts.
146 ///
147 /// The function to run will be named 0, which can be constructed with
148 /// `Hash::EMPTY`.
149 pub(crate) script: bool,
150 /// When running tests, include std tests.
151 pub(crate) test_std: bool,
152 /// Enable lowering optimizations.
153 pub(crate) lowering: u8,
154 /// Print source tree.
155 pub(crate) print_tree: bool,
156 /// Maximum depth expansions - macros, template literals and format
157 /// specifications - are allowed to nest to.
158 ///
159 /// An expansion produces a tree of its own, which lowering walks by
160 /// recursing since it cannot park a foreign tree in its frames, so this has
161 /// to stay well under what the native stack can take. Nesting of around 25
162 /// exhausted an 8 MiB stack in an unoptimised build when this was measured.
163 pub(crate) max_macro_depth: usize,
164 /// Maximum depth of the work stacks used by the compiler.
165 ///
166 /// The compiler performs its tree walks over explicit heap allocated work
167 /// stacks rather than the call stack. This bounds how large those are
168 /// allowed to grow, so that pathological input is reported as a diagnostic
169 /// instead of exhausting memory.
170 pub(crate) max_depth: usize,
171 /// Maximum depth of the syntax tree a macro parses its input into.
172 ///
173 /// The syntax tree parser recurses, and so does everything which walks what
174 /// it produced, so this is much smaller than [`Options::max_depth`] and
175 /// chains count towards it as well as nesting.
176 pub(crate) max_ast_depth: usize,
177 /// Maximum nesting of a constant.
178 ///
179 /// A constant is evaluated into a `ConstValue`, which is a recursive
180 /// structure - it is built, walked and dropped by recursing over it - so how
181 /// deeply a constant nests is bounded separately from everything else.
182 /// [`Options::max_depth`] can lower this bound but not raise it, since
183 /// raising it would trade a diagnostic for a stack overflow.
184 pub(crate) max_const_depth: usize,
185 /// Maximum number of components an item path may have.
186 ///
187 /// Every level of lexical nesting - a block, a closure, a nested item -
188 /// pushes a component onto the path of the item being built, and allocating
189 /// an item hashes and stores its whole path. So a file which nests `n` deep
190 /// costs `O(n^2)` in both time and memory even though nothing about it is
191 /// otherwise unusual. Bounding the path bounds that.
192 pub(crate) max_item_depth: usize,
193 /// Maximum number of imports which may be traversed while resolving a path.
194 ///
195 /// Imports are followed by recursing, since an import may point at another
196 /// import.
197 pub(crate) max_import_depth: usize,
198 /// The number of instructions a single constant evaluation is allowed to
199 /// execute before it is aborted.
200 ///
201 /// Constants are compiled and run in a virtual machine, so this is what
202 /// stops one which does not terminate.
203 pub(crate) const_budget: usize,
204 /// Rune format options.
205 pub(crate) fmt: FmtOptions,
206}
207
208impl Options {
209 /// The default options.
210 pub(crate) const DEFAULT: Options = Options {
211 link_checks: true,
212 memoize_instance_fn: true,
213 debug_info: true,
214 macros: true,
215 bytecode: false,
216 script: false,
217 test_std: false,
218 lowering: 0,
219 print_tree: false,
220 max_macro_depth: 16,
221 max_depth: 65536,
222 max_ast_depth: 64,
223 max_const_depth: 128,
224 max_item_depth: 128,
225 max_import_depth: 128,
226 const_budget: 1_000_000,
227 fmt: FmtOptions::DEFAULT,
228 };
229
230 /// Construct lossy rune options from the `RUNEFLAGS` environment variable.
231 pub fn from_default_env() -> Result<Self, ParseOptionError> {
232 #[allow(unused_mut)]
233 let mut options = Self::DEFAULT;
234
235 #[cfg(feature = "std")]
236 {
237 /// The environment variable where runeflags are loaded from.
238 static ENV: &str = "RUNEFLAGS";
239
240 if let Some(value) = std::env::var_os(ENV) {
241 let value = value.to_string_lossy();
242 options.parse_option_with(&value, Some(ENV))?;
243 }
244 }
245
246 Ok(options)
247 }
248
249 /// Get a list and documentation for all available compiler options.
250 pub fn available() -> &'static [OptionMeta] {
251 static BOOL: &str = "true, false";
252 static VALUES: &[OptionMeta] = &[
253 OptionMeta {
254 key: "link-checks",
255 unstable: false,
256 doc: &docstring! {
257 /// Perform link-time checks to ensure that
258 /// function hashes which are referenced during
259 /// compilation exist.
260 },
261 default: "true",
262 options: BOOL,
263 },
264 OptionMeta {
265 key: "memoize-instance-fn",
266 unstable: false,
267 doc: &docstring! {
268 /// Memoize the instance function in a loop.
269 },
270 default: "true",
271 options: BOOL,
272 },
273 OptionMeta {
274 key: "debug-info",
275 unstable: false,
276 doc: &docstring! {
277 /// Include debug information when compiling.
278 ///
279 /// This provides better diagnostics, but also
280 /// increases memory usage.
281 },
282 default: "true",
283 options: BOOL,
284 },
285 OptionMeta {
286 key: "macros",
287 unstable: false,
288 doc: &docstring! {
289 /// Support macro expansion.
290 },
291 default: "true",
292 options: BOOL,
293 },
294 OptionMeta {
295 key: "bytecode",
296 unstable: true,
297 doc: &docstring! {
298 /// Make use of bytecode, which might make
299 /// compilation units smaller.
300 },
301 default: "false",
302 options: BOOL,
303 },
304 OptionMeta {
305 key: "function-body",
306 unstable: true,
307 doc: &docstring! {
308 /// Causes sources to be treated as-if they were
309 /// function bodies, rather than modules.
310 },
311 default: "false",
312 options: BOOL,
313 },
314 OptionMeta {
315 key: "test-std",
316 unstable: true,
317 doc: &docstring! {
318 /// When running tests, includes tests found in the
319 /// standard library.
320 },
321 default: "false",
322 options: BOOL,
323 },
324 OptionMeta {
325 key: "lowering",
326 unstable: true,
327 doc: &docstring! {
328 /// Enable lowering optimizations.
329 ///
330 /// Supports a value of 0-1 with increasingly higher
331 /// levels of optimizations applied.
332 ///
333 /// Enabling a higher level results in better code
334 /// generation, but contributes to compilation times.
335 },
336 default: "0",
337 options: "0-1",
338 },
339 OptionMeta {
340 key: "print-tree",
341 unstable: false,
342 doc: &docstring! {
343 /// Print the parsed source tree when formatting to
344 /// standard output.
345 ///
346 /// Only avialable when the `std` feature is enabled.
347 },
348 default: "false",
349 options: BOOL,
350 },
351 OptionMeta {
352 key: "max-macro-depth",
353 unstable: true,
354 doc: &docstring! {
355 /// Maximum depth expansions - macros, template
356 /// literals and format specifications - may nest
357 /// to.
358 ///
359 /// An expansion produces a tree of its own, which
360 /// lowering walks by recursing, so this stays well
361 /// under what the native stack can take.
362 },
363 default: "16",
364 options: "<number>",
365 },
366 OptionMeta {
367 key: "max-depth",
368 unstable: true,
369 doc: &docstring! {
370 /// Maximum depth of the work stacks used by the
371 /// compiler.
372 ///
373 /// The compiler walks trees over explicit heap
374 /// allocated work stacks rather than the call stack,
375 /// both for chained expressions and for lexical
376 /// nesting. This bounds how large those are allowed
377 /// to grow, so pathological input is reported as a
378 /// diagnostic instead of exhausting memory.
379 },
380 default: "65536",
381 options: "<number>",
382 },
383 OptionMeta {
384 key: "max-ast-depth",
385 unstable: true,
386 doc: &docstring! {
387 /// Maximum depth of the syntax tree a macro parses
388 /// its input into.
389 ///
390 /// Unlike the rest of the compiler, this parser
391 /// recurses, and so does everything which walks
392 /// what it produced. So this is much smaller than
393 /// `max-depth`, and chains count towards it as well
394 /// as nesting.
395 },
396 default: "64",
397 options: "<number>",
398 },
399 OptionMeta {
400 key: "max-item-depth",
401 unstable: true,
402 doc: &docstring! {
403 /// Maximum number of components an item path may
404 /// have.
405 ///
406 /// Every level of lexical nesting pushes a
407 /// component onto the path of the item being
408 /// built, and allocating an item hashes and stores
409 /// its whole path, so nesting `n` deep costs
410 /// `O(n^2)`. This bounds the path so that a file
411 /// which nests pathologically is reported as a
412 /// diagnostic instead.
413 },
414 default: "128",
415 options: "<number>",
416 },
417 OptionMeta {
418 key: "max-const-depth",
419 unstable: true,
420 doc: &docstring! {
421 /// Maximum nesting of a constant.
422 ///
423 /// A constant is evaluated into a value which is
424 /// built, walked and dropped by recursing over it,
425 /// so how deeply one nests is bounded separately
426 /// from everything else. `max-depth` can lower this
427 /// bound but not raise it, since raising it would
428 /// trade a diagnostic for a stack overflow.
429 },
430 default: "128",
431 options: "<number>",
432 },
433 OptionMeta {
434 key: "max-import-depth",
435 unstable: true,
436 doc: &docstring! {
437 /// Maximum number of imports which may be traversed
438 /// while resolving a path, since an import may
439 /// point at another import.
440 },
441 default: "128",
442 options: "<number>",
443 },
444 OptionMeta {
445 key: "const-budget",
446 unstable: true,
447 doc: &docstring! {
448 /// The number of instructions a single constant
449 /// evaluation may execute before it is aborted.
450 ///
451 /// Constants are compiled and run in a virtual
452 /// machine, so this is what stops one which does
453 /// not terminate.
454 },
455 default: "1000000",
456 options: "<number>",
457 },
458 OptionMeta {
459 key: "fmt.error-recovery",
460 unstable: true,
461 doc: &docstring! {
462 /// Perform error recovery when formatting.
463 ///
464 /// This allows code to be formatted even if it
465 /// contains invalid syntax.
466 },
467 default: "false",
468 options: BOOL,
469 },
470 OptionMeta {
471 key: "fmt.force-newline",
472 unstable: true,
473 doc: &docstring! {
474 /// Force newline at end of document.
475 },
476 default: "true",
477 options: BOOL,
478 },
479 OptionMeta {
480 key: "fmt.indent",
481 unstable: true,
482 doc: &docstring! {
483 /// Number of spaces to indent with, or `tab` to indent
484 /// using tabs.
485 },
486 default: "4",
487 options: "<number>, tab",
488 },
489 ];
490
491 VALUES
492 }
493
494 /// Parse a compiler option. This is the function which parses the
495 /// `<option>[=<value>]` syntax, which is used by among other things the
496 /// Rune CLI with the `-O <option>[=<value>]` option.
497 ///
498 /// It can be used to consistenly parse a collection of options by other
499 /// programs as well.
500 pub fn parse_option(&mut self, option: &str) -> Result<(), ParseOptionError> {
501 self.parse_option_with(option, None)
502 }
503
504 fn parse_option_with(
505 &mut self,
506 option: &str,
507 env: Option<&'static str>,
508 ) -> Result<(), ParseOptionError> {
509 for option in option.split(',') {
510 let option = option.trim();
511
512 let (head, tail) = if let Some((head, tail)) = option.trim().split_once('=') {
513 (head.trim(), Some(tail.trim()))
514 } else {
515 (option.trim(), None)
516 };
517
518 match head {
519 "memoize-instance-fn" => {
520 self.memoize_instance_fn = tail.is_none_or(|s| s == "true");
521 }
522 "debug-info" => {
523 self.debug_info = tail.is_none_or(|s| s == "true");
524 }
525 "link-checks" => {
526 self.link_checks = tail.is_none_or(|s| s == "true");
527 }
528 "macros" => {
529 self.macros = tail.is_none_or(|s| s == "true");
530 }
531 "bytecode" => {
532 self.bytecode = tail.is_none_or(|s| s == "true");
533 }
534 "function-body" => {
535 self.script = tail.is_none_or(|s| s == "true");
536 }
537 "test-std" => {
538 self.test_std = tail.is_none_or(|s| s == "true");
539 }
540 "lowering" => {
541 self.lowering = match tail {
542 Some("0") | None => 0,
543 Some("1") => 1,
544 _ => {
545 return Err(ParseOptionError {
546 env,
547 option: option.into(),
548 })
549 }
550 };
551 }
552 "print-tree" if cfg!(feature = "std") => {
553 self.print_tree = tail.is_none_or(|s| s == "true");
554 }
555 "max-macro-depth" => {
556 let Some(Ok(number)) = tail.map(str::parse) else {
557 return Err(ParseOptionError {
558 env,
559 option: option.into(),
560 });
561 };
562
563 self.max_macro_depth = number;
564 }
565 "max-depth" => {
566 let Some(Ok(number)) = tail.map(str::parse) else {
567 return Err(ParseOptionError {
568 env,
569 option: option.into(),
570 });
571 };
572
573 self.max_depth = number;
574 }
575 "max-ast-depth" => {
576 let Some(Ok(number)) = tail.map(str::parse) else {
577 return Err(ParseOptionError {
578 env,
579 option: option.into(),
580 });
581 };
582
583 self.max_ast_depth = number;
584 }
585 "max-item-depth" => {
586 let Some(Ok(number)) = tail.map(str::parse) else {
587 return Err(ParseOptionError {
588 env,
589 option: option.into(),
590 });
591 };
592
593 self.max_item_depth = number;
594 }
595 "max-const-depth" => {
596 let Some(Ok(number)) = tail.map(str::parse) else {
597 return Err(ParseOptionError {
598 env,
599 option: option.into(),
600 });
601 };
602
603 self.max_const_depth = number;
604 }
605 "max-import-depth" => {
606 let Some(Ok(number)) = tail.map(str::parse) else {
607 return Err(ParseOptionError {
608 env,
609 option: option.into(),
610 });
611 };
612
613 self.max_import_depth = number;
614 }
615 "const-budget" => {
616 let Some(Ok(number)) = tail.map(str::parse) else {
617 return Err(ParseOptionError {
618 env,
619 option: option.into(),
620 });
621 };
622
623 self.const_budget = number;
624 }
625 other => {
626 let Some((head, sub)) = other.split_once('.') else {
627 return Err(ParseOptionError {
628 env,
629 option: option.into(),
630 });
631 };
632
633 let head = head.trim();
634 let sub = sub.trim();
635
636 match head {
637 "fmt" => {
638 self.fmt.parse_option_with(option, sub, tail, env)?;
639 }
640 _ => {
641 return Err(ParseOptionError {
642 env,
643 option: option.into(),
644 });
645 }
646 }
647 }
648 }
649 }
650
651 Ok(())
652 }
653
654 /// Enable the test configuration flag.
655 #[inline]
656 pub fn test(&mut self, _enabled: bool) {
657 // ignored
658 }
659
660 /// Set if debug info is enabled or not. Defaults to `true`.
661 #[inline]
662 pub fn debug_info(&mut self, enabled: bool) {
663 self.debug_info = enabled;
664 }
665
666 /// Set if link checks are enabled or not. Defaults to `true`. This will
667 /// cause compilation to fail if an instruction references a function which
668 /// does not exist.
669 #[inline]
670 pub fn link_checks(&mut self, enabled: bool) {
671 self.link_checks = enabled;
672 }
673
674 /// Set if macros are enabled or not. Defaults to `false`.
675 #[inline]
676 pub fn macros(&mut self, enabled: bool) {
677 self.macros = enabled;
678 }
679
680 /// Set if bytecode caching is enabled or not. Defaults to `false`.
681 #[inline]
682 pub fn bytecode(&mut self, enabled: bool) {
683 self.bytecode = enabled;
684 }
685
686 /// Memoize the instance function in a loop. Defaults to `false`.
687 #[inline]
688 pub fn memoize_instance_fn(&mut self, enabled: bool) {
689 self.memoize_instance_fn = enabled;
690 }
691
692 /// Whether to build sources as scripts where the source is executed like a
693 /// function body.
694 #[inline]
695 pub fn script(&mut self, enabled: bool) {
696 self.script = enabled;
697 }
698}
699
700impl Default for Options {
701 #[inline]
702 fn default() -> Self {
703 Options::DEFAULT
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::Options;
710
711 #[test]
712 fn parse_nested_option_value() {
713 let mut options = Options::DEFAULT;
714 assert!(options.fmt.force_newline);
715 options.parse_option("fmt.force-newline=false").unwrap();
716 assert!(!options.fmt.force_newline);
717 options.parse_option("fmt.force-newline=true").unwrap();
718 assert!(options.fmt.force_newline);
719 }
720
721 /// Every value an option says it takes has to be one it takes.
722 ///
723 /// The values are what `--list-options` prints, so one which is listed and
724 /// then rejected sends whoever reads the listing to an error.
725 #[test]
726 fn every_listed_option_value_parses() {
727 for meta in Options::available() {
728 let mut values = rust_alloc::vec::Vec::new();
729
730 for value in meta.options.split(',').map(str::trim) {
731 match value {
732 // A placeholder for anything of that shape rather than a
733 // value in its own right.
734 "<number>" => values.push(rust_alloc::string::ToString::to_string(&"1")),
735 // An inclusive range of numbers.
736 value if value.contains('-') => {
737 let (start, end) = value.split_once('-').unwrap();
738 let start: u32 = start.parse().expect("range start should be a number");
739 let end: u32 = end.parse().expect("range end should be a number");
740
741 for n in start..=end {
742 values.push(rust_alloc::format!("{n}"));
743 }
744 }
745 value => values.push(rust_alloc::string::ToString::to_string(&value)),
746 }
747 }
748
749 // The default is a value like any other.
750 values.push(rust_alloc::string::ToString::to_string(&meta.default));
751
752 for value in values {
753 let option = rust_alloc::format!("{}={value}", meta.key);
754 let mut options = Options::DEFAULT;
755
756 assert!(
757 options.parse_option(&option).is_ok(),
758 "`{option}` is listed for `{}` but is not accepted",
759 meta.key
760 );
761 }
762 }
763 }
764}