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