//! `tvix-eval` implements the evaluation of the Nix programming language in //! Tvix. //! //! It is designed to allow users to use Nix as a versatile language for //! different use-cases. //! //! This module exports the high-level functions and types needed for evaluating //! Nix code and interacting with the language's data structures. //! //! Nix has several language features that make use of impurities (such as //! reading from the NIX_PATH environment variable, or interacting with files). //! These features are optional and the API of this crate exposes functionality //! for controlling how they work. pub mod builtins; mod chunk; mod compiler; mod errors; mod io; pub mod observer; mod opcode; mod pretty_ast; mod source; mod spans; mod systems; mod upvalues; mod value; mod vm; mod warnings; mod nix_search_path; #[cfg(all(test, feature = "arbitrary"))] mod properties; #[cfg(test)] mod test_utils; #[cfg(test)] mod tests; use std::collections::HashMap; 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::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::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; /// 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: SourceCode, builtins: Vec<(&'static str, Value)>, src_builtins: Vec<(&'static str, &'static str)>, env: Option<&'env HashMap>, io_handle: IO, enable_import: bool, strict: bool, nix_path: Option, compiler_observer: Option<&'co mut dyn CompilerObserver>, runtime_observer: Option<&'ro mut dyn RuntimeObserver>, } // 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: SourceCode::default(), enable_import: false, io_handle, builtins, src_builtins: vec![], env: None, strict: false, nix_path: None, compiler_observer: None, runtime_observer: None, } } pub fn build(self) -> Evaluation<'co, 'ro, 'env, IO> { Evaluation { source_map: self.source_map, builtins: self.builtins, src_builtins: self.src_builtins, env: self.env, io_handle: self.io_handle, enable_import: self.enable_import, strict: self.strict, nix_path: self.nix_path, compiler_observer: self.compiler_observer, runtime_observer: self.runtime_observer, } } pub fn io_handle(self, io_handle: IO2) -> EvaluationBuilder<'co, 'ro, 'env, IO2> { EvaluationBuilder { io_handle, source_map: self.source_map, builtins: self.builtins, src_builtins: self.src_builtins, env: self.env, enable_import: self.enable_import, strict: self.strict, 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) } pub fn add_builtins(mut self, builtins: I) -> Self where I: IntoIterator, { self.builtins.extend(builtins); self } pub fn with_strict(self, strict: bool) -> Self { Self { strict, ..self } } pub fn strict(self) -> Self { self.with_strict(true) } pub fn add_src_builtin(mut self, name: &'static str, src: &'static str) -> Self { self.src_builtins.push((name, src)); self } pub fn nix_path(self, nix_path: Option) -> Self { Self { nix_path, ..self } } pub fn env(self, env: Option<&'env HashMap>) -> 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(&self) -> &SourceCode { &self.source_map } } impl<'co, 'ro, 'env> EvaluationBuilder<'co, 'ro, 'env, Box> { /// 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).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>) -> Self { self.io_handle = io.unwrap_or_else(|| Box::new(StdIO) as Box); 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(); } 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, '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. builtins: Vec<(&'static str, Value)>, /// Set of builtins that are implemented in Nix itself and should /// be compiled and inserted in the builtins set. src_builtins: Vec<(&'static str, &'static str)>, /// Top-level variables to define in the evaluation env: Option<&'env HashMap>, /// Implementation of file-IO to use during evaluation, e.g. for /// impure builtins. /// /// Defaults to [`DummyIO`] if not set explicitly. 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`. enable_import: bool, /// Determines whether the returned value should be strictly /// evaluated, that is whether its list and attribute set elements /// should be forced recursively. strict: bool, /// (optional) Nix search path, e.g. the value of `NIX_PATH` used /// for resolving items on the search path (such as ``). nix_path: Option, /// (optional) compiler observer for reporting on compilation /// details, like the emitted bytecode. compiler_observer: Option<&'co mut dyn CompilerObserver>, /// (optional) runtime observer, for reporting on execution steps /// of Nix code. runtime_observer: Option<&'ro mut dyn RuntimeObserver>, } /// Result of evaluating a piece of Nix code. If evaluation succeeded, a value /// will be present (and potentially some warnings!). If evaluation failed, /// errors will be present. #[derive(Debug, Default)] pub struct EvaluationResult { /// Nix value that the code evaluated to. pub value: Option, /// Errors that occured during evaluation (if any). pub errors: Vec, /// Warnings that occured during evaluation. Warnings are not critical, but /// should be addressed either to modernise code or improve performance. pub warnings: Vec, /// AST node that was parsed from the code (on success only). pub expr: Option, } impl<'co, 'ro, 'env, IO> Evaluation<'co, 'ro, 'env, IO> { pub fn builder(io_handle: IO) -> EvaluationBuilder<'co, 'ro, 'env, IO> { EvaluationBuilder::new(io_handle) } } impl<'co, 'ro, 'env> Evaluation<'co, 'ro, 'env, Box> { #[cfg(feature = "impure")] pub fn builder_impure() -> EvaluationBuilder<'co, 'ro, 'env, Box> { EvaluationBuilder::new_impure() } pub fn builder_pure() -> EvaluationBuilder<'co, 'ro, 'env, Box> { EvaluationBuilder::new_pure() } } impl<'co, 'ro, 'env, IO> Evaluation<'co, 'ro, 'env, IO> where IO: AsRef + '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) /// This does not *run* the code, it only provides analysis (errors and /// warnings) of the compiler. pub fn compile_only( mut self, code: impl AsRef, location: Option, ) -> EvaluationResult { let mut result = EvaluationResult::default(); let source = self.source_map(); let location_str = location .as_ref() .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|| "[code]".into()); let file = source.add_file(location_str, code.as_ref().to_string()); let mut noop_observer = observer::NoOpObserver::default(); let compiler_observer = self.compiler_observer.take().unwrap_or(&mut noop_observer); parse_compile_internal( &mut result, code.as_ref(), file, location, source, self.builtins, self.src_builtins, self.env, self.enable_import, compiler_observer, ); result } /// Evaluate 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) pub fn evaluate( mut self, code: impl AsRef, location: Option, ) -> EvaluationResult { let mut result = EvaluationResult::default(); let source = self.source_map(); let location_str = location .as_ref() .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|| "[code]".into()); let file = source.add_file(location_str, code.as_ref().to_string()); 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( &mut result, code.as_ref(), file.clone(), location, source.clone(), self.builtins, self.src_builtins, self.env, self.enable_import, compiler_observer, ) { None => return result, Some(cr) => cr, }; // If bytecode was returned, there were no errors and the // code is safe to execute. let nix_path = self .nix_path .as_ref() .and_then(|s| match nix_search_path::NixSearchPath::from_str(s) { Ok(path) => Some(path), Err(err) => { result.warnings.push(EvalWarning { kind: WarningKind::InvalidNixPath(err.to_string()), span: file.span, }); None } }) .unwrap_or_default(); let runtime_observer = self.runtime_observer.take().unwrap_or(&mut noop_observer); let vm_result = run_lambda( nix_path, self.io_handle, runtime_observer, source.clone(), globals, lambda, self.strict, ); match vm_result { Ok(mut runtime_result) => { result.warnings.append(&mut runtime_result.warnings); if let Value::Catchable(inner) = runtime_result.value { result.errors.push(Error::new( ErrorKind::CatchableError(*inner), file.span, source, )); return result; } result.value = Some(runtime_result.value); } Err(err) => { result.errors.push(err); } } result } } /// Internal helper function for common parsing & compilation logic /// between the public functions. #[allow(clippy::too_many_arguments)] // internal API, no point making an indirection type fn parse_compile_internal( result: &mut EvaluationResult, code: &str, file: Arc, location: Option, source: SourceCode, builtins: Vec<(&'static str, Value)>, src_builtins: Vec<(&'static str, &'static str)>, env: Option<&HashMap>, enable_import: bool, compiler_observer: &mut dyn CompilerObserver, ) -> Option<(Rc, Rc)> { let parsed = rnix::ast::Root::parse(code); let parse_errors = parsed.errors(); if !parse_errors.is_empty() { result.errors.push(Error::new( ErrorKind::ParseErrors(parse_errors.to_vec()), file.span, source, )); return None; } // At this point we know that the code is free of parse errors and // we can continue to compile it. The expression is persisted in // 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, env, &source, &file, compiler_observer, ) { Ok(result) => result, Err(err) => { result.errors.push(err); return None; } }; result.warnings = compiler_result.warnings; result.errors.extend(compiler_result.errors); // Short-circuit if errors exist at this point (do not pass broken // bytecode to the runtime). if !result.errors.is_empty() { return None; } // 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)) }