diff options
Diffstat (limited to 'tvix/eval/src/lib.rs')
-rw-r--r-- | tvix/eval/src/lib.rs | 427 |
1 files changed, 325 insertions, 102 deletions
diff --git a/tvix/eval/src/lib.rs b/tvix/eval/src/lib.rs index 398da4d6e22e..a4ef3f01e40a 100644 --- a/tvix/eval/src/lib.rs +++ b/tvix/eval/src/lib.rs @@ -36,80 +36,342 @@ mod test_utils; #[cfg(test)] mod tests; +use rustc_hash::FxHashMap; use std::path::PathBuf; use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; -use crate::compiler::GlobalsMap; use crate::observer::{CompilerObserver, RuntimeObserver}; use crate::value::Lambda; use crate::vm::run_lambda; // Re-export the public interface used by other crates. -pub use crate::compiler::{compile, prepare_globals, CompilationOutput}; +pub use crate::compiler::{compile, prepare_globals, CompilationOutput, GlobalsMap}; pub use crate::errors::{AddContext, CatchableErrorKind, Error, ErrorKind, EvalResult}; pub use crate::io::{DummyIO, EvalIO, FileType}; pub use crate::pretty_ast::pretty_print_expr; pub use crate::source::SourceCode; pub use crate::value::{NixContext, NixContextElement}; -pub use crate::vm::generators; +pub use crate::vm::{generators, EvalMode}; pub use crate::warnings::{EvalWarning, WarningKind}; pub use builtin_macros; +use smol_str::SmolStr; pub use crate::value::{Builtin, CoercionKind, NixAttrs, NixList, NixString, Value}; #[cfg(feature = "impure")] pub use crate::io::StdIO; +struct BuilderBuiltins { + builtins: Vec<(&'static str, Value)>, + src_builtins: Vec<(&'static str, &'static str)>, +} + +enum BuilderGlobals { + Builtins(BuilderBuiltins), + Globals(Rc<GlobalsMap>), +} + +/// Builder for building an [`Evaluation`]. +/// +/// Construct an [`EvaluationBuilder`] by calling one of: +/// +/// - [`Evaluation::builder`] / [`EvaluationBuilder::new`] +/// - [`Evaluation::builder_impure`] [`EvaluationBuilder::new_impure`] +/// - [`Evaluation::builder_pure`] [`EvaluationBuilder::new_pure`] +/// +/// Then configure the fields by calling the various methods on [`EvaluationBuilder`], and finally +/// call [`build`](Self::build) to construct an [`Evaluation`] +pub struct EvaluationBuilder<'co, 'ro, 'env, IO> { + source_map: Option<SourceCode>, + globals: BuilderGlobals, + env: Option<&'env FxHashMap<SmolStr, Value>>, + io_handle: IO, + enable_import: bool, + mode: EvalMode, + nix_path: Option<String>, + compiler_observer: Option<&'co mut dyn CompilerObserver>, + runtime_observer: Option<&'ro mut dyn RuntimeObserver>, +} + +impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> +where + IO: AsRef<dyn EvalIO> + 'static, +{ + /// Build an [`Evaluation`] based on the configuration in this builder. + /// + /// This: + /// + /// - Adds a `"storeDir"` builtin containing the store directory of the configured IO handle + /// - Sets up globals based on the configured builtins + /// - Copies all other configured fields to the [`Evaluation`] + pub fn build(self) -> Evaluation<'co, 'ro, 'env, IO> { + let source_map = self.source_map.unwrap_or_default(); + + let globals = match self.globals { + BuilderGlobals::Globals(globals) => globals, + BuilderGlobals::Builtins(BuilderBuiltins { + mut builtins, + src_builtins, + }) => { + // Insert a storeDir builtin *iff* a store directory is present. + if let Some(store_dir) = self.io_handle.as_ref().store_dir() { + builtins.push(("storeDir", store_dir.into())); + } + + crate::compiler::prepare_globals( + builtins, + src_builtins, + source_map.clone(), + self.enable_import, + ) + } + }; + + Evaluation { + source_map, + globals, + env: self.env, + io_handle: self.io_handle, + mode: self.mode, + nix_path: self.nix_path, + compiler_observer: self.compiler_observer, + runtime_observer: self.runtime_observer, + } + } +} + +// NOTE(aspen): The methods here are intentionally incomplete; feel free to add new ones (ideally +// with similar naming conventions to the ones already present) but don't expose fields publically! +impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> { + pub fn new(io_handle: IO) -> Self { + let mut builtins = builtins::pure_builtins(); + builtins.extend(builtins::placeholders()); // these are temporary + + Self { + source_map: None, + enable_import: false, + io_handle, + globals: BuilderGlobals::Builtins(BuilderBuiltins { + builtins, + src_builtins: vec![], + }), + env: None, + mode: Default::default(), + nix_path: None, + compiler_observer: None, + runtime_observer: None, + } + } + + pub fn io_handle<IO2>(self, io_handle: IO2) -> EvaluationBuilder<'co, 'ro, 'env, IO2> { + EvaluationBuilder { + io_handle, + source_map: self.source_map, + globals: self.globals, + env: self.env, + enable_import: self.enable_import, + mode: self.mode, + nix_path: self.nix_path, + compiler_observer: self.compiler_observer, + runtime_observer: self.runtime_observer, + } + } + + pub fn with_enable_import(self, enable_import: bool) -> Self { + Self { + enable_import, + ..self + } + } + + pub fn disable_import(self) -> Self { + self.with_enable_import(false) + } + + pub fn enable_import(self) -> Self { + self.with_enable_import(true) + } + + fn builtins_mut(&mut self) -> &mut BuilderBuiltins { + match &mut self.globals { + BuilderGlobals::Builtins(builtins) => builtins, + BuilderGlobals::Globals(_) => { + panic!("Cannot modify builtins on an EvaluationBuilder with globals configured") + } + } + } + + /// Add additional builtins (represented as tuples of name and [`Value`]) to this evaluation + /// builder. + /// + /// # Panics + /// + /// Panics if this evaluation builder has had globals set via [`with_globals`] + pub fn add_builtins<I>(mut self, builtins: I) -> Self + where + I: IntoIterator<Item = (&'static str, Value)>, + { + self.builtins_mut().builtins.extend(builtins); + self + } + + /// Add additional builtins that are implemented in Nix source code (represented as tuples of + /// name and nix source) to this evaluation builder. + /// + /// # Panics + /// + /// Panics if this evaluation builder has had globals set via [`with_globals`] + pub fn add_src_builtin(mut self, name: &'static str, src: &'static str) -> Self { + self.builtins_mut().src_builtins.push((name, src)); + self + } + + /// Set the globals for this evaluation builder to a previously-constructed globals map. + /// Intended to allow sharing globals across multiple evaluations (eg for the REPL). + /// + /// Discards any builtins previously configured via [`add_builtins`] and [`add_src_builtins`]. + /// If either of those methods is called on the evaluation builder after this one, they will + /// panic. + pub fn with_globals(self, globals: Rc<GlobalsMap>) -> Self { + Self { + globals: BuilderGlobals::Globals(globals), + ..self + } + } + + pub fn with_source_map(self, source_map: SourceCode) -> Self { + debug_assert!( + self.source_map.is_none(), + "Cannot set the source_map on an EvaluationBuilder twice" + ); + Self { + source_map: Some(source_map), + ..self + } + } + + pub fn mode(self, mode: EvalMode) -> Self { + Self { mode, ..self } + } + + pub fn nix_path(self, nix_path: Option<String>) -> Self { + Self { nix_path, ..self } + } + + pub fn env(self, env: Option<&'env FxHashMap<SmolStr, Value>>) -> Self { + Self { env, ..self } + } + + pub fn compiler_observer( + self, + compiler_observer: Option<&'co mut dyn CompilerObserver>, + ) -> Self { + Self { + compiler_observer, + ..self + } + } + + pub fn set_compiler_observer( + &mut self, + compiler_observer: Option<&'co mut dyn CompilerObserver>, + ) { + self.compiler_observer = compiler_observer; + } + + pub fn runtime_observer(self, runtime_observer: Option<&'ro mut dyn RuntimeObserver>) -> Self { + Self { + runtime_observer, + ..self + } + } + + pub fn set_runtime_observer(&mut self, runtime_observer: Option<&'ro mut dyn RuntimeObserver>) { + self.runtime_observer = runtime_observer; + } +} + +impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> { + pub fn source_map(&mut self) -> &SourceCode { + self.source_map.get_or_insert_with(SourceCode::default) + } +} + +impl<'co, 'ro, 'env> EvaluationBuilder<'co, 'ro, 'env, Box<dyn EvalIO>> { + /// Initialize an `Evaluation`, without the import statement available, and + /// all IO operations stubbed out. + pub fn new_pure() -> Self { + Self::new(Box::new(DummyIO) as Box<dyn EvalIO>).with_enable_import(false) + } + + #[cfg(feature = "impure")] + /// Configure an `Evaluation` to have impure features available + /// with the given I/O implementation. + /// + /// If no I/O implementation is supplied, [`StdIO`] is used by + /// default. + pub fn enable_impure(mut self, io: Option<Box<dyn EvalIO>>) -> Self { + self.io_handle = io.unwrap_or_else(|| Box::new(StdIO) as Box<dyn EvalIO>); + self.enable_import = true; + self.builtins_mut() + .builtins + .extend(builtins::impure_builtins()); + + // Make `NIX_PATH` resolutions work by default, unless the + // user already overrode this with something else. + if self.nix_path.is_none() { + self.nix_path = std::env::var("NIX_PATH").ok(); + } + self + } + + #[cfg(feature = "impure")] + /// Initialise an `Evaluation`, with all impure features turned on by default. + pub fn new_impure() -> Self { + Self::new_pure().enable_impure(None) + } +} + /// An `Evaluation` represents how a piece of Nix code is evaluated. It can be /// instantiated and configured directly, or it can be accessed through the /// various simplified helper methods available below. /// /// Public fields are intended to be set by the caller. Setting all /// fields is optional. -pub struct Evaluation<'co, 'ro, IO> { +pub struct Evaluation<'co, 'ro, 'env, IO> { /// Source code map used for error reporting. source_map: SourceCode, - /// Set of all builtins that should be available during the - /// evaluation. - /// - /// This defaults to all pure builtins. Users might want to add - /// the set of impure builtins, or other custom builtins. - pub builtins: Vec<(&'static str, Value)>, + /// Set of all global values available at the top-level scope + globals: Rc<GlobalsMap>, - /// Set of builtins that are implemented in Nix itself and should - /// be compiled and inserted in the builtins set. - pub src_builtins: Vec<(&'static str, &'static str)>, + /// Top-level variables to define in the evaluation + env: Option<&'env FxHashMap<SmolStr, Value>>, /// Implementation of file-IO to use during evaluation, e.g. for /// impure builtins. /// /// Defaults to [`DummyIO`] if not set explicitly. - pub io_handle: IO, - - /// Determines whether the `import` builtin should be made - /// available. Note that this depends on the `io_handle` being - /// able to read the files specified as arguments to `import`. - pub enable_import: bool, + io_handle: IO, - /// Determines whether the returned value should be strictly - /// evaluated, that is whether its list and attribute set elements - /// should be forced recursively. - pub strict: bool, + /// Specification for how to handle top-level values returned by evaluation + /// + /// See the documentation for [`EvalMode`] for more information. + mode: EvalMode, /// (optional) Nix search path, e.g. the value of `NIX_PATH` used /// for resolving items on the search path (such as `<nixpkgs>`). - pub nix_path: Option<String>, + nix_path: Option<String>, /// (optional) compiler observer for reporting on compilation /// details, like the emitted bytecode. - pub compiler_observer: Option<&'co mut dyn CompilerObserver>, + compiler_observer: Option<&'co mut dyn CompilerObserver>, /// (optional) runtime observer, for reporting on execution steps /// of Nix code. - pub runtime_observer: Option<&'ro mut dyn RuntimeObserver>, + runtime_observer: Option<&'ro mut dyn RuntimeObserver>, } /// Result of evaluating a piece of Nix code. If evaluation succeeded, a value @@ -131,73 +393,44 @@ pub struct EvaluationResult { pub expr: Option<rnix::ast::Expr>, } -impl<'co, 'ro, IO> Evaluation<'co, 'ro, IO> -where - IO: AsRef<dyn EvalIO> + 'static, -{ - /// Initialize an `Evaluation`. - pub fn new(io_handle: IO, enable_import: bool) -> Self { - let mut builtins = builtins::pure_builtins(); - builtins.extend(builtins::placeholders()); // these are temporary +impl<'co, 'ro, 'env, IO> Evaluation<'co, 'ro, 'env, IO> { + /// Make a new [builder][] for configuring an evaluation + /// + /// [builder]: EvaluationBuilder + pub fn builder(io_handle: IO) -> EvaluationBuilder<'co, 'ro, 'env, IO> { + EvaluationBuilder::new(io_handle) + } - Self { - source_map: SourceCode::default(), - enable_import, - io_handle, - builtins, - src_builtins: vec![], - strict: false, - nix_path: None, - compiler_observer: None, - runtime_observer: None, - } + /// Clone the reference to the map of Nix globals for this evaluation. If [`Value`]s are shared + /// across subsequent [`Evaluation`]s, it is important that those evaluations all have the same + /// underlying globals map. + pub fn globals(&self) -> Rc<GlobalsMap> { + self.globals.clone() } -} -impl<'co, 'ro> Evaluation<'co, 'ro, Box<dyn EvalIO>> { - /// Initialize an `Evaluation`, without the import statement available, and - /// all IO operations stubbed out. - pub fn new_pure() -> Self { - Self::new(Box::new(DummyIO) as Box<dyn EvalIO>, false) + /// Clone the reference to the contained source code map. This is used after an evaluation for + /// pretty error printing. Also, if [`Value`]s are shared across subsequent [`Evaluation`]s, it + /// is important that those evaluations all have the same underlying source code map. + pub fn source_map(&self) -> SourceCode { + self.source_map.clone() } +} +impl<'co, 'ro, 'env> Evaluation<'co, 'ro, 'env, Box<dyn EvalIO>> { #[cfg(feature = "impure")] - /// Configure an `Evaluation` to have impure features available - /// with the given I/O implementation. - /// - /// If no I/O implementation is supplied, [`StdIO`] is used by - /// default. - pub fn enable_impure(&mut self, io: Option<Box<dyn EvalIO>>) { - self.io_handle = io.unwrap_or_else(|| Box::new(StdIO) as Box<dyn EvalIO>); - self.enable_import = true; - self.builtins.extend(builtins::impure_builtins()); - - // Make `NIX_PATH` resolutions work by default, unless the - // user already overrode this with something else. - if self.nix_path.is_none() { - self.nix_path = std::env::var("NIX_PATH").ok(); - } + pub fn builder_impure() -> EvaluationBuilder<'co, 'ro, 'env, Box<dyn EvalIO>> { + EvaluationBuilder::new_impure() } - #[cfg(feature = "impure")] - /// Initialise an `Evaluation`, with all impure features turned on by default. - pub fn new_impure() -> Self { - let mut eval = Self::new_pure(); - eval.enable_impure(None); - eval + pub fn builder_pure() -> EvaluationBuilder<'co, 'ro, 'env, Box<dyn EvalIO>> { + EvaluationBuilder::new_pure() } } -impl<'co, 'ro, IO> Evaluation<'co, 'ro, IO> +impl<'co, 'ro, 'env, IO> Evaluation<'co, 'ro, 'env, IO> where IO: AsRef<dyn EvalIO> + 'static, { - /// Clone the reference to the contained source code map. This is used after - /// an evaluation for pretty error printing. - pub fn source_map(&self) -> SourceCode { - self.source_map.clone() - } - /// Only compile the provided source code, at an optional location of the /// source code (i.e. path to the file it was read from; used for error /// reporting, and for resolving relative paths in impure functions) @@ -227,9 +460,8 @@ where file, location, source, - self.builtins, - self.src_builtins, - self.enable_import, + self.globals, + self.env, compiler_observer, ); @@ -257,20 +489,14 @@ where let mut noop_observer = observer::NoOpObserver::default(); let compiler_observer = self.compiler_observer.take().unwrap_or(&mut noop_observer); - // Insert a storeDir builtin *iff* a store directory is present. - if let Some(store_dir) = self.io_handle.as_ref().store_dir() { - self.builtins.push(("storeDir", store_dir.into())); - } - - let (lambda, globals) = match parse_compile_internal( + let lambda = match parse_compile_internal( &mut result, code.as_ref(), file.clone(), location, source.clone(), - self.builtins, - self.src_builtins, - self.enable_import, + self.globals.clone(), + self.env, compiler_observer, ) { None => return result, @@ -302,9 +528,9 @@ where self.io_handle, runtime_observer, source.clone(), - globals, + self.globals, lambda, - self.strict, + self.mode, ); match vm_result { @@ -339,11 +565,10 @@ fn parse_compile_internal( file: Arc<codemap::File>, location: Option<PathBuf>, source: SourceCode, - builtins: Vec<(&'static str, Value)>, - src_builtins: Vec<(&'static str, &'static str)>, - enable_import: bool, + globals: Rc<GlobalsMap>, + env: Option<&FxHashMap<SmolStr, Value>>, compiler_observer: &mut dyn CompilerObserver, -) -> Option<(Rc<Lambda>, Rc<GlobalsMap>)> { +) -> Option<Rc<Lambda>> { let parsed = rnix::ast::Root::parse(code); let parse_errors = parsed.errors(); @@ -361,13 +586,11 @@ fn parse_compile_internal( // the result, in case the caller needs it for something. result.expr = parsed.tree().expr(); - let builtins = - crate::compiler::prepare_globals(builtins, src_builtins, source.clone(), enable_import); - let compiler_result = match compiler::compile( result.expr.as_ref().unwrap(), location, - builtins, + globals, + env, &source, &file, compiler_observer, @@ -390,5 +613,5 @@ fn parse_compile_internal( // Return the lambda (for execution) and the globals map (to // ensure the invariant that the globals outlive the runtime). - Some((compiler_result.lambda, compiler_result.globals)) + Some(compiler_result.lambda) } |