tvix_eval/
lib.rs

1//! `tvix-eval` implements the evaluation of the Nix programming language in
2//! Tvix.
3//!
4//! It is designed to allow users to use Nix as a versatile language for
5//! different use-cases.
6//!
7//! This module exports the high-level functions and types needed for evaluating
8//! Nix code and interacting with the language's data structures.
9//!
10//! Nix has several language features that make use of impurities (such as
11//! reading from the NIX_PATH environment variable, or interacting with files).
12//! These features are optional and the API of this crate exposes functionality
13//! for controlling how they work.
14
15pub mod builtins;
16mod chunk;
17mod compiler;
18mod errors;
19mod io;
20pub mod observer;
21mod opcode;
22mod pretty_ast;
23mod source;
24mod spans;
25mod systems;
26mod upvalues;
27mod value;
28mod vm;
29mod warnings;
30
31mod nix_search_path;
32#[cfg(all(test, feature = "arbitrary"))]
33mod properties;
34#[cfg(test)]
35mod test_utils;
36#[cfg(test)]
37mod tests;
38
39use rustc_hash::FxHashMap;
40use std::path::PathBuf;
41use std::rc::Rc;
42use std::str::FromStr;
43use std::sync::Arc;
44
45use crate::observer::{CompilerObserver, RuntimeObserver};
46use crate::value::Lambda;
47use crate::vm::run_lambda;
48
49// Re-export the public interface used by other crates.
50pub use crate::compiler::{compile, prepare_globals, CompilationOutput, GlobalsMap};
51pub use crate::errors::{AddContext, CatchableErrorKind, Error, ErrorKind, EvalResult};
52pub use crate::io::{DummyIO, EvalIO, FileType};
53pub use crate::nix_search_path::NixSearchPath;
54pub use crate::pretty_ast::pretty_print_expr;
55pub use crate::source::SourceCode;
56pub use crate::value::{NixContext, NixContextElement};
57pub use crate::vm::{generators, EvalMode};
58pub use crate::warnings::{EvalWarning, WarningKind};
59pub use builtin_macros;
60use smol_str::SmolStr;
61
62pub use crate::value::{Builtin, CoercionKind, NixAttrs, NixList, NixString, Value};
63
64#[cfg(feature = "impure")]
65pub use crate::io::StdIO;
66
67struct BuilderBuiltins {
68    builtins: Vec<(&'static str, Value)>,
69    src_builtins: Vec<(&'static str, &'static str)>,
70}
71
72enum BuilderGlobals {
73    Builtins(BuilderBuiltins),
74    Globals(Rc<GlobalsMap>),
75}
76
77/// Builder for building an [`Evaluation`].
78///
79/// Construct an [`EvaluationBuilder`] by calling one of:
80///
81/// - [`Evaluation::builder`] / [`EvaluationBuilder::new`]
82/// - [`Evaluation::builder_impure`] [`EvaluationBuilder::new_impure`]
83/// - [`Evaluation::builder_pure`] [`EvaluationBuilder::new_pure`]
84///
85/// Then configure the fields by calling the various methods on [`EvaluationBuilder`], and finally
86/// call [`build`](Self::build) to construct an [`Evaluation`]
87pub struct EvaluationBuilder<'co, 'ro, 'env> {
88    source_map: Option<SourceCode>,
89    globals: BuilderGlobals,
90    env: Option<&'env FxHashMap<SmolStr, Value>>,
91    io_handle: Rc<dyn EvalIO>,
92    enable_import: bool,
93    mode: EvalMode,
94    nix_path: Option<String>,
95    compiler_observer: Option<&'co mut dyn CompilerObserver>,
96    runtime_observer: Option<&'ro mut dyn RuntimeObserver>,
97}
98
99impl<'co, 'ro, 'env> EvaluationBuilder<'co, 'ro, 'env> {
100    /// Build an [`Evaluation`] based on the configuration in this builder.
101    ///
102    /// This:
103    ///
104    /// - Adds a `"storeDir"` builtin containing the store directory of the configured IO handle
105    /// - Sets up globals based on the configured builtins
106    /// - Copies all other configured fields to the [`Evaluation`]
107    pub fn build(self) -> Evaluation<'co, 'ro, 'env> {
108        let source_map = self.source_map.unwrap_or_default();
109
110        let globals = match self.globals {
111            BuilderGlobals::Globals(globals) => globals,
112            BuilderGlobals::Builtins(BuilderBuiltins {
113                mut builtins,
114                src_builtins,
115            }) => {
116                // Insert a storeDir builtin *iff* a store directory is present.
117                if let Some(store_dir) = self.io_handle.as_ref().store_dir() {
118                    builtins.push(("storeDir", store_dir.into()));
119                }
120
121                crate::compiler::prepare_globals(
122                    builtins,
123                    src_builtins,
124                    source_map.clone(),
125                    self.enable_import,
126                )
127            }
128        };
129
130        Evaluation {
131            source_map,
132            globals,
133            env: self.env,
134            io_handle: self.io_handle,
135            mode: self.mode,
136            nix_path: self.nix_path,
137            compiler_observer: self.compiler_observer,
138            runtime_observer: self.runtime_observer,
139        }
140    }
141}
142
143// NOTE(aspen): The methods here are intentionally incomplete; feel free to add new ones (ideally
144// with similar naming conventions to the ones already present) but don't expose fields publically!
145impl<'co, 'ro, 'env> EvaluationBuilder<'co, 'ro, 'env> {
146    pub fn new(io_handle: Rc<dyn EvalIO>) -> Self {
147        let mut builtins = builtins::pure_builtins();
148        builtins.extend(builtins::placeholders()); // these are temporary
149
150        Self {
151            source_map: None,
152            enable_import: false,
153            io_handle,
154            globals: BuilderGlobals::Builtins(BuilderBuiltins {
155                builtins,
156                src_builtins: vec![],
157            }),
158            env: None,
159            mode: Default::default(),
160            nix_path: None,
161            compiler_observer: None,
162            runtime_observer: None,
163        }
164    }
165
166    pub fn io_handle(self, io_handle: Rc<dyn EvalIO>) -> EvaluationBuilder<'co, 'ro, 'env> {
167        EvaluationBuilder {
168            io_handle,
169            source_map: self.source_map,
170            globals: self.globals,
171            env: self.env,
172            enable_import: self.enable_import,
173            mode: self.mode,
174            nix_path: self.nix_path,
175            compiler_observer: self.compiler_observer,
176            runtime_observer: self.runtime_observer,
177        }
178    }
179
180    pub fn with_enable_import(self, enable_import: bool) -> Self {
181        Self {
182            enable_import,
183            ..self
184        }
185    }
186
187    pub fn disable_import(self) -> Self {
188        self.with_enable_import(false)
189    }
190
191    pub fn enable_import(self) -> Self {
192        self.with_enable_import(true)
193    }
194
195    fn builtins_mut(&mut self) -> &mut BuilderBuiltins {
196        match &mut self.globals {
197            BuilderGlobals::Builtins(builtins) => builtins,
198            BuilderGlobals::Globals(_) => {
199                panic!("Cannot modify builtins on an EvaluationBuilder with globals configured")
200            }
201        }
202    }
203
204    /// Add additional builtins (represented as tuples of name and [`Value`]) to this evaluation
205    /// builder.
206    ///
207    /// # Panics
208    ///
209    /// Panics if this evaluation builder has had globals set via [`with_globals`]
210    pub fn add_builtins<I>(mut self, builtins: I) -> Self
211    where
212        I: IntoIterator<Item = (&'static str, Value)>,
213    {
214        self.builtins_mut().builtins.extend(builtins);
215        self
216    }
217
218    /// Add additional builtins that are implemented in Nix source code (represented as tuples of
219    /// name and nix source) to this evaluation builder.
220    ///
221    /// # Panics
222    ///
223    /// Panics if this evaluation builder has had globals set via [`with_globals`]
224    pub fn add_src_builtin(mut self, name: &'static str, src: &'static str) -> Self {
225        self.builtins_mut().src_builtins.push((name, src));
226        self
227    }
228
229    /// Set the globals for this evaluation builder to a previously-constructed globals map.
230    /// Intended to allow sharing globals across multiple evaluations (eg for the REPL).
231    ///
232    /// Discards any builtins previously configured via [`add_builtins`] and [`add_src_builtins`].
233    /// If either of those methods is called on the evaluation builder after this one, they will
234    /// panic.
235    pub fn with_globals(self, globals: Rc<GlobalsMap>) -> Self {
236        Self {
237            globals: BuilderGlobals::Globals(globals),
238            ..self
239        }
240    }
241
242    pub fn with_source_map(self, source_map: SourceCode) -> Self {
243        debug_assert!(
244            self.source_map.is_none(),
245            "Cannot set the source_map on an EvaluationBuilder twice"
246        );
247        Self {
248            source_map: Some(source_map),
249            ..self
250        }
251    }
252
253    pub fn mode(self, mode: EvalMode) -> Self {
254        Self { mode, ..self }
255    }
256
257    pub fn nix_path(self, nix_path: Option<String>) -> Self {
258        Self { nix_path, ..self }
259    }
260
261    pub fn env(self, env: Option<&'env FxHashMap<SmolStr, Value>>) -> Self {
262        Self { env, ..self }
263    }
264
265    pub fn compiler_observer(
266        self,
267        compiler_observer: Option<&'co mut dyn CompilerObserver>,
268    ) -> Self {
269        Self {
270            compiler_observer,
271            ..self
272        }
273    }
274
275    pub fn set_compiler_observer(
276        &mut self,
277        compiler_observer: Option<&'co mut dyn CompilerObserver>,
278    ) {
279        self.compiler_observer = compiler_observer;
280    }
281
282    pub fn runtime_observer(self, runtime_observer: Option<&'ro mut dyn RuntimeObserver>) -> Self {
283        Self {
284            runtime_observer,
285            ..self
286        }
287    }
288
289    pub fn set_runtime_observer(&mut self, runtime_observer: Option<&'ro mut dyn RuntimeObserver>) {
290        self.runtime_observer = runtime_observer;
291    }
292}
293
294impl EvaluationBuilder<'_, '_, '_> {
295    pub fn source_map(&mut self) -> &SourceCode {
296        self.source_map.get_or_insert_with(SourceCode::default)
297    }
298}
299
300impl EvaluationBuilder<'_, '_, '_> {
301    /// Initialize an `Evaluation`, without the import statement available, and
302    /// all IO operations stubbed out.
303    pub fn new_pure() -> Self {
304        Self::new(Rc::new(DummyIO) as Rc<dyn EvalIO>).with_enable_import(false)
305    }
306
307    #[cfg(feature = "impure")]
308    /// Configure an `Evaluation` to have impure features available
309    /// with the given I/O implementation.
310    ///
311    /// If no I/O implementation is supplied, [`StdIO`] is used by
312    /// default.
313    pub fn enable_impure(mut self, io: Option<Rc<dyn EvalIO>>) -> Self {
314        self.io_handle = io.unwrap_or_else(|| Rc::new(StdIO) as Rc<dyn EvalIO>);
315        self.enable_import = true;
316        self.builtins_mut()
317            .builtins
318            .extend(builtins::impure_builtins());
319
320        // Make `NIX_PATH` resolutions work by default, unless the
321        // user already overrode this with something else.
322        if self.nix_path.is_none() {
323            self.nix_path = std::env::var("NIX_PATH").ok();
324        }
325        self
326    }
327
328    #[cfg(feature = "impure")]
329    /// Initialise an `Evaluation`, with all impure features turned on by default.
330    pub fn new_impure() -> Self {
331        Self::new_pure().enable_impure(None)
332    }
333}
334
335/// An `Evaluation` represents how a piece of Nix code is evaluated. It can be
336/// instantiated and configured directly, or it can be accessed through the
337/// various simplified helper methods available below.
338///
339/// Public fields are intended to be set by the caller. Setting all
340/// fields is optional.
341pub struct Evaluation<'co, 'ro, 'env> {
342    /// Source code map used for error reporting.
343    source_map: SourceCode,
344
345    /// Set of all global values available at the top-level scope
346    globals: Rc<GlobalsMap>,
347
348    /// Top-level variables to define in the evaluation
349    env: Option<&'env FxHashMap<SmolStr, Value>>,
350
351    /// Implementation of file-IO to use during evaluation, e.g. for
352    /// impure builtins.
353    ///
354    /// Defaults to [`DummyIO`] if not set explicitly.
355    io_handle: Rc<dyn EvalIO>,
356
357    /// Specification for how to handle top-level values returned by evaluation
358    ///
359    /// See the documentation for [`EvalMode`] for more information.
360    mode: EvalMode,
361
362    /// (optional) Nix search path, e.g. the value of `NIX_PATH` used
363    /// for resolving items on the search path (such as `<nixpkgs>`).
364    nix_path: Option<String>,
365
366    /// (optional) compiler observer for reporting on compilation
367    /// details, like the emitted bytecode.
368    compiler_observer: Option<&'co mut dyn CompilerObserver>,
369
370    /// (optional) runtime observer, for reporting on execution steps
371    /// of Nix code.
372    runtime_observer: Option<&'ro mut dyn RuntimeObserver>,
373}
374
375/// Result of evaluating a piece of Nix code. If evaluation succeeded, a value
376/// will be present (and potentially some warnings!). If evaluation failed,
377/// errors will be present.
378#[derive(Debug, Default)]
379pub struct EvaluationResult {
380    /// Nix value that the code evaluated to.
381    pub value: Option<Value>,
382
383    /// Errors that occured during evaluation (if any).
384    pub errors: Vec<Error>,
385
386    /// Warnings that occured during evaluation. Warnings are not critical, but
387    /// should be addressed either to modernise code or improve performance.
388    pub warnings: Vec<EvalWarning>,
389
390    /// AST node that was parsed from the code (on success only).
391    pub expr: Option<rnix::ast::Expr>,
392}
393
394impl<'co, 'ro, 'env> Evaluation<'co, 'ro, 'env> {
395    /// Make a new [builder][] for configuring an evaluation
396    ///
397    /// [builder]: EvaluationBuilder
398    pub fn builder(io_handle: Rc<dyn EvalIO>) -> EvaluationBuilder<'co, 'ro, 'env> {
399        EvaluationBuilder::new(io_handle)
400    }
401
402    /// Clone the reference to the map of Nix globals for this evaluation. If [`Value`]s are shared
403    /// across subsequent [`Evaluation`]s, it is important that those evaluations all have the same
404    /// underlying globals map.
405    pub fn globals(&self) -> Rc<GlobalsMap> {
406        self.globals.clone()
407    }
408
409    /// Clone the reference to the contained source code map. This is used after an evaluation for
410    /// pretty error printing. Also, if [`Value`]s are shared across subsequent [`Evaluation`]s, it
411    /// is important that those evaluations all have the same underlying source code map.
412    pub fn source_map(&self) -> SourceCode {
413        self.source_map.clone()
414    }
415}
416
417impl<'co, 'ro, 'env> Evaluation<'co, 'ro, 'env> {
418    #[cfg(feature = "impure")]
419    pub fn builder_impure() -> EvaluationBuilder<'co, 'ro, 'env> {
420        EvaluationBuilder::new_impure()
421    }
422
423    pub fn builder_pure() -> EvaluationBuilder<'co, 'ro, 'env> {
424        EvaluationBuilder::new_pure()
425    }
426}
427
428impl Evaluation<'_, '_, '_> {
429    /// Only compile the provided source code, at an optional location of the
430    /// source code (i.e. path to the file it was read from; used for error
431    /// reporting, and for resolving relative paths in impure functions)
432    /// This does not *run* the code, it only provides analysis (errors and
433    /// warnings) of the compiler.
434    pub fn compile_only(
435        mut self,
436        code: impl AsRef<str>,
437        location: Option<PathBuf>,
438    ) -> EvaluationResult {
439        let mut result = EvaluationResult::default();
440        let source = self.source_map();
441
442        let location_str = location
443            .as_ref()
444            .map(|p| p.to_string_lossy().to_string())
445            .unwrap_or_else(|| "[code]".into());
446
447        let file = source.add_file(location_str, code.as_ref().to_string());
448
449        let mut noop_observer = observer::NoOpObserver::default();
450        let compiler_observer = self.compiler_observer.take().unwrap_or(&mut noop_observer);
451
452        parse_compile_internal(
453            &mut result,
454            code.as_ref(),
455            file,
456            location,
457            source,
458            self.globals,
459            self.env,
460            compiler_observer,
461        );
462
463        result
464    }
465
466    /// Evaluate the provided source code, at an optional location of the source
467    /// code (i.e. path to the file it was read from; used for error reporting,
468    /// and for resolving relative paths in impure functions)
469    pub fn evaluate(
470        mut self,
471        code: impl AsRef<str>,
472        location: Option<PathBuf>,
473    ) -> EvaluationResult {
474        let mut result = EvaluationResult::default();
475        let source = self.source_map();
476
477        let location_str = location
478            .as_ref()
479            .map(|p| p.to_string_lossy().to_string())
480            .unwrap_or_else(|| "[code]".into());
481
482        let file = source.add_file(location_str, code.as_ref().to_string());
483
484        let mut noop_observer = observer::NoOpObserver::default();
485        let compiler_observer = self.compiler_observer.take().unwrap_or(&mut noop_observer);
486
487        let lambda = match parse_compile_internal(
488            &mut result,
489            code.as_ref(),
490            file.clone(),
491            location,
492            source.clone(),
493            self.globals.clone(),
494            self.env,
495            compiler_observer,
496        ) {
497            None => return result,
498            Some(cr) => cr,
499        };
500
501        // If bytecode was returned, there were no errors and the
502        // code is safe to execute.
503
504        let nix_path = self
505            .nix_path
506            .as_ref()
507            .and_then(|s| match nix_search_path::NixSearchPath::from_str(s) {
508                Ok(path) => Some(path),
509                Err(err) => {
510                    result.warnings.push(EvalWarning {
511                        kind: WarningKind::InvalidNixPath(err.to_string()),
512                        span: file.span,
513                    });
514                    None
515                }
516            })
517            .unwrap_or_default();
518
519        let runtime_observer = self.runtime_observer.take().unwrap_or(&mut noop_observer);
520
521        let vm_result = run_lambda(
522            nix_path,
523            self.io_handle,
524            runtime_observer,
525            source.clone(),
526            self.globals,
527            lambda,
528            self.mode,
529        );
530
531        match vm_result {
532            Ok(mut runtime_result) => {
533                result.warnings.append(&mut runtime_result.warnings);
534                if let Value::Catchable(inner) = runtime_result.value {
535                    result.errors.push(Error::new(
536                        ErrorKind::CatchableError(*inner),
537                        file.span,
538                        source,
539                    ));
540                    return result;
541                }
542
543                result.value = Some(runtime_result.value);
544            }
545            Err(err) => {
546                result.errors.push(err);
547            }
548        }
549
550        result
551    }
552}
553
554/// Internal helper function for common parsing & compilation logic
555/// between the public functions.
556#[allow(clippy::too_many_arguments)] // internal API, no point making an indirection type
557fn parse_compile_internal(
558    result: &mut EvaluationResult,
559    code: &str,
560    file: Arc<codemap::File>,
561    location: Option<PathBuf>,
562    source: SourceCode,
563    globals: Rc<GlobalsMap>,
564    env: Option<&FxHashMap<SmolStr, Value>>,
565    compiler_observer: &mut dyn CompilerObserver,
566) -> Option<Rc<Lambda>> {
567    let parsed = rnix::ast::Root::parse(code);
568    let parse_errors = parsed.errors();
569
570    if !parse_errors.is_empty() {
571        result.errors.push(Error::new(
572            ErrorKind::ParseErrors(parse_errors.to_vec()),
573            file.span,
574            source,
575        ));
576        return None;
577    }
578
579    // At this point we know that the code is free of parse errors and
580    // we can continue to compile it. The expression is persisted in
581    // the result, in case the caller needs it for something.
582    result.expr = parsed.tree().expr();
583
584    let compiler_result = match compiler::compile(
585        result.expr.as_ref().unwrap(),
586        location,
587        globals,
588        env,
589        &source,
590        &file,
591        compiler_observer,
592    ) {
593        Ok(result) => result,
594        Err(err) => {
595            result.errors.push(err);
596            return None;
597        }
598    };
599
600    result.warnings = compiler_result.warnings;
601    result.errors.extend(compiler_result.errors);
602
603    // Short-circuit if errors exist at this point (do not pass broken
604    // bytecode to the runtime).
605    if !result.errors.is_empty() {
606        return None;
607    }
608
609    // Return the lambda (for execution) and the globals map (to
610    // ensure the invariant that the globals outlive the runtime).
611    Some(compiler_result.lambda)
612}