Skip to main content

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}