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    /// Use the v2 compiler.
157    pub(crate) v2: bool,
158    /// Maximum macro depth.
159    pub(crate) max_macro_depth: usize,
160    /// Rune format options.
161    pub(crate) fmt: FmtOptions,
162}
163
164impl Options {
165    /// The default options.
166    pub(crate) const DEFAULT: Options = Options {
167        link_checks: true,
168        memoize_instance_fn: true,
169        debug_info: true,
170        macros: true,
171        bytecode: false,
172        script: false,
173        test_std: false,
174        lowering: 0,
175        print_tree: false,
176        v2: false,
177        max_macro_depth: 64,
178        fmt: FmtOptions::DEFAULT,
179    };
180
181    /// Construct lossy rune options from the `RUNEFLAGS` environment variable.
182    pub fn from_default_env() -> Result<Self, ParseOptionError> {
183        #[allow(unused_mut)]
184        let mut options = Self::DEFAULT;
185
186        #[cfg(feature = "std")]
187        {
188            /// The environment variable where runeflags are loaded from.
189            static ENV: &str = "RUNEFLAGS";
190
191            if let Some(value) = std::env::var_os(ENV) {
192                let value = value.to_string_lossy();
193                options.parse_option_with(&value, Some(ENV))?;
194            }
195        }
196
197        Ok(options)
198    }
199
200    /// Get a list and documentation for all available compiler options.
201    pub fn available() -> &'static [OptionMeta] {
202        static BOOL: &str = "true, false";
203        static VALUES: &[OptionMeta] = &[
204            OptionMeta {
205                key: "link-checks",
206                unstable: false,
207                doc: &docstring! {
208                    /// Perform link-time checks to ensure that
209                    /// function hashes which are referenced during
210                    /// compilation exist.
211                },
212                default: "true",
213                options: BOOL,
214            },
215            OptionMeta {
216                key: "memoize-instance-fn",
217                unstable: false,
218                doc: &docstring! {
219                    /// Memoize the instance function in a loop.
220                },
221                default: "true",
222                options: BOOL,
223            },
224            OptionMeta {
225                key: "debug-info",
226                unstable: false,
227                doc: &docstring! {
228                    /// Include debug information when compiling.
229                    ///
230                    /// This provides better diagnostics, but also
231                    /// increases memory usage.
232                },
233                default: "true",
234                options: BOOL,
235            },
236            OptionMeta {
237                key: "macros",
238                unstable: false,
239                doc: &docstring! {
240                    /// Support macro expansion.
241                },
242                default: "true",
243                options: BOOL,
244            },
245            OptionMeta {
246                key: "bytecode",
247                unstable: true,
248                doc: &docstring! {
249                    /// Make use of bytecode, which might make
250                    /// compilation units smaller.
251                },
252                default: "false",
253                options: BOOL,
254            },
255            OptionMeta {
256                key: "function-body",
257                unstable: true,
258                doc: &docstring! {
259                    /// Causes sources to be treated as-if they were
260                    /// function bodies, rather than modules.
261                },
262                default: "false",
263                options: BOOL,
264            },
265            OptionMeta {
266                key: "test-std",
267                unstable: true,
268                doc: &docstring! {
269                    /// When running tests, includes tests found in the
270                    /// standard library.
271                },
272                default: "false",
273                options: BOOL,
274            },
275            OptionMeta {
276                key: "lowering",
277                unstable: true,
278                doc: &docstring! {
279                    /// Enable lowering optimizations.
280                    ///
281                    /// Supports a value of 0-3 with increasingly higher
282                    /// levels of optimizations applied.
283                    ///
284                    /// Enabling a higher level results in better code
285                    /// generation, but contributes to compilation times.
286                },
287                default: "0",
288                options: "0-3",
289            },
290            OptionMeta {
291                key: "print-tree",
292                unstable: false,
293                doc: &docstring! {
294                    /// Print the parsed source tree when formatting to
295                    /// standard output.
296                    ///
297                    /// Only avialable when the `std` feature is enabled.
298                },
299                default: "false",
300                options: BOOL,
301            },
302            OptionMeta {
303                key: "v2",
304                unstable: true,
305                doc: &docstring! {
306                    /// Use the v2 compiler.
307                },
308                default: "false",
309                options: BOOL,
310            },
311            OptionMeta {
312                key: "max-macro-depth",
313                unstable: true,
314                doc: &docstring! {
315                    /// Maximum supported macro depth.
316                },
317                default: "64",
318                options: "<number>",
319            },
320            OptionMeta {
321                key: "fmt.error-recovery",
322                unstable: true,
323                doc: &docstring! {
324                    /// Perform error recovery when formatting.
325                    ///
326                    /// This allows code to be formatted even if it
327                    /// contains invalid syntax.
328                },
329                default: "false",
330                options: BOOL,
331            },
332            OptionMeta {
333                key: "fmt.force-newline",
334                unstable: true,
335                doc: &docstring! {
336                    /// Force newline at end of document.
337                },
338                default: "true",
339                options: BOOL,
340            },
341            OptionMeta {
342                key: "fmt.indent",
343                unstable: true,
344                doc: &docstring! {
345                    /// Number of spaces to indent with, or `tab` to indent
346                    /// using tabs.
347                },
348                default: "4",
349                options: "<number>, tab",
350            },
351        ];
352
353        VALUES
354    }
355
356    /// Parse a compiler option. This is the function which parses the
357    /// `<option>[=<value>]` syntax, which is used by among other things the
358    /// Rune CLI with the `-O <option>[=<value>]` option.
359    ///
360    /// It can be used to consistenly parse a collection of options by other
361    /// programs as well.
362    pub fn parse_option(&mut self, option: &str) -> Result<(), ParseOptionError> {
363        self.parse_option_with(option, None)
364    }
365
366    fn parse_option_with(
367        &mut self,
368        option: &str,
369        env: Option<&'static str>,
370    ) -> Result<(), ParseOptionError> {
371        for option in option.split(',') {
372            let option = option.trim();
373
374            let (head, tail) = if let Some((head, tail)) = option.trim().split_once('=') {
375                (head.trim(), Some(tail.trim()))
376            } else {
377                (option.trim(), None)
378            };
379
380            match head {
381                "memoize-instance-fn" => {
382                    self.memoize_instance_fn = tail.is_none_or(|s| s == "true");
383                }
384                "debug-info" => {
385                    self.debug_info = tail.is_none_or(|s| s == "true");
386                }
387                "link-checks" => {
388                    self.link_checks = tail.is_none_or(|s| s == "true");
389                }
390                "macros" => {
391                    self.macros = tail.is_none_or(|s| s == "true");
392                }
393                "bytecode" => {
394                    self.bytecode = tail.is_none_or(|s| s == "true");
395                }
396                "function-body" => {
397                    self.script = tail.is_none_or(|s| s == "true");
398                }
399                "test-std" => {
400                    self.test_std = tail.is_none_or(|s| s == "true");
401                }
402                "lowering" => {
403                    self.lowering = match tail {
404                        Some("0") | None => 0,
405                        Some("1") => 1,
406                        _ => {
407                            return Err(ParseOptionError {
408                                env,
409                                option: option.into(),
410                            })
411                        }
412                    };
413                }
414                "print-tree" if cfg!(feature = "std") => {
415                    self.print_tree = tail.is_none_or(|s| s == "true");
416                }
417                "v2" => {
418                    self.v2 = tail.is_none_or(|s| s == "true");
419                }
420                "max-macro-depth" => {
421                    let Some(Ok(number)) = tail.map(str::parse) else {
422                        return Err(ParseOptionError {
423                            env,
424                            option: option.into(),
425                        });
426                    };
427
428                    self.max_macro_depth = number;
429                }
430                other => {
431                    let Some((head, sub)) = other.split_once('.') else {
432                        return Err(ParseOptionError {
433                            env,
434                            option: option.into(),
435                        });
436                    };
437
438                    let head = head.trim();
439                    let sub = sub.trim();
440
441                    match head {
442                        "fmt" => {
443                            self.fmt.parse_option_with(option, sub, tail, env)?;
444                        }
445                        _ => {
446                            return Err(ParseOptionError {
447                                env,
448                                option: option.into(),
449                            });
450                        }
451                    }
452                }
453            }
454        }
455
456        Ok(())
457    }
458
459    /// Enable the test configuration flag.
460    #[inline]
461    pub fn test(&mut self, _enabled: bool) {
462        // ignored
463    }
464
465    /// Set if debug info is enabled or not. Defaults to `true`.
466    #[inline]
467    pub fn debug_info(&mut self, enabled: bool) {
468        self.debug_info = enabled;
469    }
470
471    /// Set if link checks are enabled or not. Defaults to `true`. This will
472    /// cause compilation to fail if an instruction references a function which
473    /// does not exist.
474    #[inline]
475    pub fn link_checks(&mut self, enabled: bool) {
476        self.link_checks = enabled;
477    }
478
479    /// Set if macros are enabled or not. Defaults to `false`.
480    #[inline]
481    pub fn macros(&mut self, enabled: bool) {
482        self.macros = enabled;
483    }
484
485    /// Set if bytecode caching is enabled or not. Defaults to `false`.
486    #[inline]
487    pub fn bytecode(&mut self, enabled: bool) {
488        self.bytecode = enabled;
489    }
490
491    /// Memoize the instance function in a loop. Defaults to `false`.
492    #[inline]
493    pub fn memoize_instance_fn(&mut self, enabled: bool) {
494        self.memoize_instance_fn = enabled;
495    }
496
497    /// Whether to build sources as scripts where the source is executed like a
498    /// function body.
499    #[inline]
500    pub fn script(&mut self, enabled: bool) {
501        self.script = enabled;
502    }
503}
504
505impl Default for Options {
506    #[inline]
507    fn default() -> Self {
508        Options::DEFAULT
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::Options;
515
516    #[test]
517    fn parse_nested_option_value() {
518        let mut options = Options::DEFAULT;
519        assert!(options.fmt.force_newline);
520        options.parse_option("fmt.force-newline=false").unwrap();
521        assert!(!options.fmt.force_newline);
522        options.parse_option("fmt.force-newline=true").unwrap();
523        assert!(options.fmt.force_newline);
524    }
525}