Something went wrong. Try again.
forked from tvlfyi/tvix
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613//! `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 rustc_hash::FxHashMap;use std::path::PathBuf;use std::rc::Rc;use std::str::FromStr;use std::sync::Arc;
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::{CompilationOutput, GlobalsMap, compile, prepare_globals};pub use crate::errors::{AddContext, CatchableErrorKind, Error, ErrorKind, EvalResult};pub use crate::io::{DummyIO, EvalIO, FileType};pub use crate::nix_search_path::NixSearchPath;pub use crate::pretty_ast::pretty_print_expr;pub use crate::source::SourceCode;pub use crate::value::{NixContext, NixContextElement};pub use crate::vm::{EvalMode, 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;
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> { source_map: Option<SourceCode>, globals: BuilderGlobals, env: Option<&'env FxHashMap<SmolStr, Value>>, io_handle: Rc<dyn EvalIO>, 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> EvaluationBuilder<'co, 'ro, 'env> { /// 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> { 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> EvaluationBuilder<'co, 'ro, 'env> { pub fn new(io_handle: Rc<dyn EvalIO>) -> 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(self, io_handle: Rc<dyn EvalIO>) -> EvaluationBuilder<'co, 'ro, 'env> { 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 EvaluationBuilder<'_, '_, '_> { pub fn source_map(&mut self) -> &SourceCode { self.source_map.get_or_insert_with(SourceCode::default) }}
impl EvaluationBuilder<'_, '_, '_> { /// Initialize an `Evaluation`, without the import statement available, and /// all IO operations stubbed out. pub fn new_pure() -> Self { Self::new(Rc::new(DummyIO) as Rc<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<Rc<dyn EvalIO>>) -> Self { self.io_handle = io.unwrap_or_else(|| Rc::new(StdIO) as Rc<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, 'env> { /// Source code map used for error reporting. source_map: SourceCode,
/// Set of all global values available at the top-level scope globals: Rc<GlobalsMap>,
/// 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. io_handle: Rc<dyn EvalIO>,
/// 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>`). nix_path: Option<String>,
/// (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<Value>,
/// Errors that occured during evaluation (if any). pub errors: Vec<Error>,
/// Warnings that occured during evaluation. Warnings are not critical, but /// should be addressed either to modernise code or improve performance. pub warnings: Vec<EvalWarning>,
/// AST node that was parsed from the code (on success only). pub expr: Option<rnix::ast::Expr>,}
impl<'co, 'ro, 'env> Evaluation<'co, 'ro, 'env> { /// Make a new [builder][] for configuring an evaluation /// /// [builder]: EvaluationBuilder pub fn builder(io_handle: Rc<dyn EvalIO>) -> EvaluationBuilder<'co, 'ro, 'env> { EvaluationBuilder::new(io_handle) }
/// 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() }
/// 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> { #[cfg(feature = "impure")] pub fn builder_impure() -> EvaluationBuilder<'co, 'ro, 'env> { EvaluationBuilder::new_impure() }
pub fn builder_pure() -> EvaluationBuilder<'co, 'ro, 'env> { EvaluationBuilder::new_pure() }}
impl Evaluation<'_, '_, '_> { /// 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<str>, location: Option<PathBuf>, ) -> 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.globals, self.env, 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<str>, location: Option<PathBuf>, ) -> 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);
let lambda = match parse_compile_internal( &mut result, code.as_ref(), file.clone(), location, source.clone(), self.globals.clone(), self.env, 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(), self.globals, lambda, self.mode, );
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 typefn parse_compile_internal( result: &mut EvaluationResult, code: &str, file: Arc<codemap::File>, location: Option<PathBuf>, source: SourceCode, globals: Rc<GlobalsMap>, env: Option<&FxHashMap<SmolStr, Value>>, compiler_observer: &mut dyn CompilerObserver,) -> Option<Rc<Lambda>> { 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 compiler_result = match compiler::compile( result.expr.as_ref().unwrap(), location, globals, 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)}