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