snix_eval/
lib.rs

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