about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--tvix/cli/src/lib.rs67
-rw-r--r--tvix/cli/src/main.rs4
-rw-r--r--tvix/cli/src/repl.rs34
-rw-r--r--tvix/cli/tests/.skip-tree0
-rw-r--r--tvix/cli/tests/import.nix1
-rw-r--r--tvix/cli/tests/repl.rs16
-rw-r--r--tvix/cli/tests/six.nix1
-rw-r--r--tvix/eval/src/compiler/mod.rs2
-rw-r--r--tvix/eval/src/lib.rs151
-rw-r--r--tvix/utils.nix5
10 files changed, 221 insertions, 60 deletions
diff --git a/tvix/cli/src/lib.rs b/tvix/cli/src/lib.rs
index 008593c5d84f..800ffb4e0e75 100644
--- a/tvix/cli/src/lib.rs
+++ b/tvix/cli/src/lib.rs
@@ -8,7 +8,7 @@ use tvix_build::buildservice;
 use tvix_eval::{
     builtins::impure_builtins,
     observer::{DisassemblingObserver, TracingObserver},
-    ErrorKind, EvalIO, Value,
+    ErrorKind, EvalIO, GlobalsMap, SourceCode, Value,
 };
 use tvix_glue::{
     builtins::{add_derivation_builtins, add_fetcher_builtins, add_import_builtins},
@@ -83,7 +83,13 @@ impl AllowIncomplete {
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub struct IncompleteInput;
 
+pub struct EvalResult {
+    value: Option<Value>,
+    globals: Rc<GlobalsMap>,
+}
+
 /// Interprets the given code snippet, printing out warnings and errors and returning the result
+#[allow(clippy::too_many_arguments)]
 pub fn evaluate(
     tvix_store_io: Rc<TvixStoreIO>,
     code: &str,
@@ -91,7 +97,9 @@ pub fn evaluate(
     args: &Args,
     allow_incomplete: AllowIncomplete,
     env: Option<&HashMap<SmolStr, Value>>,
-) -> Result<Option<Value>, IncompleteInput> {
+    globals: Option<Rc<GlobalsMap>>,
+    source_map: Option<SourceCode>,
+) -> Result<EvalResult, IncompleteInput> {
     let span = Span::current();
     span.pb_start();
     span.pb_set_style(&tvix_tracing::PB_SPINNER_STYLE);
@@ -102,16 +110,27 @@ pub fn evaluate(
     )) as Box<dyn EvalIO>)
     .enable_import()
     .with_strict(args.strict)
-    .add_builtins(impure_builtins())
     .env(env);
 
-    eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&tvix_store_io));
-    eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&tvix_store_io));
-    eval_builder = add_import_builtins(eval_builder, tvix_store_io);
-    eval_builder = configure_nix_path(eval_builder, &args.nix_search_path);
+    match globals {
+        Some(globals) => {
+            eval_builder = eval_builder.with_globals(globals);
+        }
+        None => {
+            eval_builder = eval_builder.add_builtins(impure_builtins());
+            eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&tvix_store_io));
+            eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&tvix_store_io));
+            eval_builder = add_import_builtins(eval_builder, tvix_store_io);
+            eval_builder = configure_nix_path(eval_builder, &args.nix_search_path);
+        }
+    };
+
+    if let Some(source_map) = source_map {
+        eval_builder = eval_builder.with_source_map(source_map);
+    }
 
     let source_map = eval_builder.source_map().clone();
-    let result = {
+    let (result, globals) = {
         let mut compiler_observer =
             DisassemblingObserver::new(source_map.clone(), std::io::stderr());
         if args.dump_bytecode {
@@ -129,7 +148,9 @@ pub fn evaluate(
         span.pb_set_message("Evaluating…");
 
         let eval = eval_builder.build();
-        eval.evaluate(code, path)
+        let globals = eval.globals();
+        let result = eval.evaluate(code, path);
+        (result, globals)
     };
 
     if allow_incomplete.allow()
@@ -160,19 +181,24 @@ pub fn evaluate(
         }
     }
 
-    Ok(result.value)
+    Ok(EvalResult {
+        globals,
+        value: result.value,
+    })
 }
 
 pub struct InterpretResult {
     output: String,
     success: bool,
+    pub(crate) globals: Option<Rc<GlobalsMap>>,
 }
 
 impl InterpretResult {
-    pub fn empty_success() -> Self {
+    pub fn empty_success(globals: Option<Rc<GlobalsMap>>) -> Self {
         Self {
             output: String::new(),
             success: true,
+            globals,
         }
     }
 
@@ -194,6 +220,7 @@ impl InterpretResult {
 /// and the result itself. The return value indicates whether
 /// evaluation succeeded.
 #[instrument(skip_all, fields(indicatif.pb_show=1))]
+#[allow(clippy::too_many_arguments)]
 pub fn interpret(
     tvix_store_io: Rc<TvixStoreIO>,
     code: &str,
@@ -202,11 +229,22 @@ pub fn interpret(
     explain: bool,
     allow_incomplete: AllowIncomplete,
     env: Option<&HashMap<SmolStr, Value>>,
+    globals: Option<Rc<GlobalsMap>>,
+    source_map: Option<SourceCode>,
 ) -> Result<InterpretResult, IncompleteInput> {
     let mut output = String::new();
-    let result = evaluate(tvix_store_io, code, path, args, allow_incomplete, env)?;
+    let result = evaluate(
+        tvix_store_io,
+        code,
+        path,
+        args,
+        allow_incomplete,
+        env,
+        globals,
+        source_map,
+    )?;
 
-    if let Some(value) = result.as_ref() {
+    if let Some(value) = result.value.as_ref() {
         if explain {
             writeln!(&mut output, "=> {}", value.explain()).unwrap();
         } else if args.raw {
@@ -219,6 +257,7 @@ pub fn interpret(
     // inform the caller about any errors
     Ok(InterpretResult {
         output,
-        success: result.is_some(),
+        success: result.value.is_some(),
+        globals: Some(result.globals),
     })
 }
diff --git a/tvix/cli/src/main.rs b/tvix/cli/src/main.rs
index 0bd3be37eeb2..f927665aeb76 100644
--- a/tvix/cli/src/main.rs
+++ b/tvix/cli/src/main.rs
@@ -75,6 +75,8 @@ fn main() {
             false,
             AllowIncomplete::RequireComplete,
             None, // TODO(aspen): Pass in --arg/--argstr here
+            None,
+            None,
         )
         .unwrap()
         .finalize()
@@ -104,6 +106,8 @@ fn run_file(io_handle: Rc<TvixStoreIO>, mut path: PathBuf, args: &Args) {
             false,
             AllowIncomplete::RequireComplete,
             None,
+            None,
+            None,
         )
         .unwrap()
         .finalize()
diff --git a/tvix/cli/src/repl.rs b/tvix/cli/src/repl.rs
index 5098fbaeedc3..6b34b6552da0 100644
--- a/tvix/cli/src/repl.rs
+++ b/tvix/cli/src/repl.rs
@@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf};
 
 use rustyline::{error::ReadlineError, Editor};
 use smol_str::SmolStr;
-use tvix_eval::Value;
+use tvix_eval::{GlobalsMap, SourceCode, Value};
 use tvix_glue::tvix_store_io::TvixStoreIO;
 
 use crate::{
@@ -92,6 +92,8 @@ pub struct Repl<'a> {
 
     io_handle: Rc<TvixStoreIO>,
     args: &'a Args,
+    source_map: SourceCode,
+    globals: Option<Rc<GlobalsMap>>,
 }
 
 impl<'a> Repl<'a> {
@@ -103,6 +105,8 @@ impl<'a> Repl<'a> {
             env: HashMap::new(),
             io_handle,
             args,
+            source_map: Default::default(),
+            globals: None,
         }
     }
 
@@ -179,7 +183,7 @@ impl<'a> Repl<'a> {
             }
             ReplCommand::Help => {
                 println!("{}", ReplCommand::HELP);
-                Ok(InterpretResult::empty_success())
+                Ok(InterpretResult::empty_success(None))
             }
             ReplCommand::Expr(input) => interpret(
                 Rc::clone(&self.io_handle),
@@ -189,6 +193,8 @@ impl<'a> Repl<'a> {
                 false,
                 AllowIncomplete::Allow,
                 Some(&self.env),
+                self.globals.clone(),
+                Some(self.source_map.clone()),
             ),
             ReplCommand::Assign(Assignment { ident, value }) => {
                 match evaluate(
@@ -198,12 +204,15 @@ impl<'a> Repl<'a> {
                     self.args,
                     AllowIncomplete::Allow,
                     Some(&self.env),
+                    self.globals.clone(),
+                    Some(self.source_map.clone()),
                 ) {
-                    Ok(Some(value)) => {
-                        self.env.insert(ident.into(), value);
-                        Ok(InterpretResult::empty_success())
+                    Ok(result) => {
+                        if let Some(value) = result.value {
+                            self.env.insert(ident.into(), value);
+                        }
+                        Ok(InterpretResult::empty_success(Some(result.globals)))
                     }
-                    Ok(None) => Ok(InterpretResult::empty_success()),
                     Err(incomplete) => Err(incomplete),
                 }
             }
@@ -215,6 +224,8 @@ impl<'a> Repl<'a> {
                 true,
                 AllowIncomplete::Allow,
                 Some(&self.env),
+                self.globals.clone(),
+                Some(self.source_map.clone()),
             ),
             ReplCommand::Print(input) => interpret(
                 Rc::clone(&self.io_handle),
@@ -227,13 +238,22 @@ impl<'a> Repl<'a> {
                 false,
                 AllowIncomplete::Allow,
                 Some(&self.env),
+                self.globals.clone(),
+                Some(self.source_map.clone()),
             ),
         };
 
         match res {
-            Ok(InterpretResult { output, .. }) => {
+            Ok(InterpretResult {
+                output,
+                globals,
+                success: _,
+            }) => {
                 self.rl.add_history_entry(input);
                 self.multiline_input = None;
+                if globals.is_some() {
+                    self.globals = globals;
+                }
                 CommandResult {
                     output,
                     continue_: true,
diff --git a/tvix/cli/tests/.skip-tree b/tvix/cli/tests/.skip-tree
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/tvix/cli/tests/.skip-tree
diff --git a/tvix/cli/tests/import.nix b/tvix/cli/tests/import.nix
new file mode 100644
index 000000000000..9ac2d0232ec8
--- /dev/null
+++ b/tvix/cli/tests/import.nix
@@ -0,0 +1 @@
+{ }: import ./six.nix { }
diff --git a/tvix/cli/tests/repl.rs b/tvix/cli/tests/repl.rs
index a14f4bff7e6a..c6644330976e 100644
--- a/tvix/cli/tests/repl.rs
+++ b/tvix/cli/tests/repl.rs
@@ -53,6 +53,22 @@ test_repl!(bind_lazy() {
     "#]];
 });
 
+test_repl!(bind_lazy_errors() {
+    r#"x = (_: "x" + 1)"# => expect![[""]];
+    "x null" => expect![[""]];
+});
+
+test_repl!(bind_referencing_import() {
+    "six = import ./tests/six.nix {}" => expect![[""]];
+    "six.six" => expect![[r#"
+        => 6 :: int
+    "#]];
+    "imported = import ./tests/import.nix"  => expect![[""]];
+    "(imported {}).six" => expect![[r#"
+        => 6 :: int
+    "#]];
+});
+
 test_repl!(deep_print() {
     "builtins.map (x: x + 1) [ 1 2 3 ]" => expect![[r#"
         => [ <CODE> <CODE> <CODE> ] :: list
diff --git a/tvix/cli/tests/six.nix b/tvix/cli/tests/six.nix
new file mode 100644
index 000000000000..d466abe06a4c
--- /dev/null
+++ b/tvix/cli/tests/six.nix
@@ -0,0 +1 @@
+{ }: { six = builtins.foldl' (x: y: x + y) 0 [ 1 2 3 ]; }
diff --git a/tvix/eval/src/compiler/mod.rs b/tvix/eval/src/compiler/mod.rs
index 1ec47599ff9c..3a25052aabb2 100644
--- a/tvix/eval/src/compiler/mod.rs
+++ b/tvix/eval/src/compiler/mod.rs
@@ -117,7 +117,7 @@ impl TrackedFormal {
 
 /// The map of globally available functions and other values that
 /// should implicitly be resolvable in the global scope.
-pub(crate) type GlobalsMap = HashMap<&'static str, Value>;
+pub type GlobalsMap = HashMap<&'static str, Value>;
 
 /// Set of builtins that (if they exist) should be made available in
 /// the global scope, meaning that they can be accessed not just
diff --git a/tvix/eval/src/lib.rs b/tvix/eval/src/lib.rs
index 00dc7918d41f..a53a2a02d140 100644
--- a/tvix/eval/src/lib.rs
+++ b/tvix/eval/src/lib.rs
@@ -42,13 +42,12 @@ use std::rc::Rc;
 use std::str::FromStr;
 use std::sync::Arc;
 
-use crate::compiler::GlobalsMap;
 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::{compile, prepare_globals, CompilationOutput};
+pub use crate::compiler::{compile, prepare_globals, CompilationOutput, GlobalsMap};
 pub use crate::errors::{AddContext, CatchableErrorKind, Error, ErrorKind, EvalResult};
 pub use crate::io::{DummyIO, EvalIO, FileType};
 pub use crate::pretty_ast::pretty_print_expr;
@@ -64,6 +63,16 @@ pub use crate::value::{Builtin, CoercionKind, NixAttrs, NixList, NixString, Valu
 #[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:
@@ -75,9 +84,8 @@ pub use crate::io::StdIO;
 /// 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, IO> {
-    source_map: SourceCode,
-    builtins: Vec<(&'static str, Value)>,
-    src_builtins: Vec<(&'static str, &'static str)>,
+    source_map: Option<SourceCode>,
+    globals: BuilderGlobals,
     env: Option<&'env HashMap<SmolStr, Value>>,
     io_handle: IO,
     enable_import: bool,
@@ -98,21 +106,31 @@ where
     /// - 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(mut self) -> Evaluation<'co, 'ro, 'env, IO> {
-        // Insert a storeDir builtin *iff* a store directory is present.
-        if let Some(store_dir) = self.io_handle.as_ref().store_dir() {
-            self.builtins.push(("storeDir", store_dir.into()));
-        }
+    pub fn build(self) -> Evaluation<'co, 'ro, 'env, IO> {
+        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()));
+                }
 
-        let globals = crate::compiler::prepare_globals(
-            self.builtins,
-            self.src_builtins,
-            self.source_map.clone(),
-            self.enable_import,
-        );
+                crate::compiler::prepare_globals(
+                    builtins,
+                    src_builtins,
+                    source_map.clone(),
+                    self.enable_import,
+                )
+            }
+        };
 
         Evaluation {
-            source_map: self.source_map,
+            source_map,
             globals,
             env: self.env,
             io_handle: self.io_handle,
@@ -132,11 +150,13 @@ impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> {
         builtins.extend(builtins::placeholders()); // these are temporary
 
         Self {
-            source_map: SourceCode::default(),
+            source_map: None,
             enable_import: false,
             io_handle,
-            builtins,
-            src_builtins: vec![],
+            globals: BuilderGlobals::Builtins(BuilderBuiltins {
+                builtins,
+                src_builtins: vec![],
+            }),
             env: None,
             strict: false,
             nix_path: None,
@@ -149,8 +169,7 @@ impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> {
         EvaluationBuilder {
             io_handle,
             source_map: self.source_map,
-            builtins: self.builtins,
-            src_builtins: self.src_builtins,
+            globals: self.globals,
             env: self.env,
             enable_import: self.enable_import,
             strict: self.strict,
@@ -175,14 +194,64 @@ impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> {
         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.extend(builtins);
+        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 with_strict(self, strict: bool) -> Self {
         Self { strict, ..self }
     }
@@ -191,11 +260,6 @@ impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> {
         self.with_strict(true)
     }
 
-    pub fn add_src_builtin(mut self, name: &'static str, src: &'static str) -> Self {
-        self.src_builtins.push((name, src));
-        self
-    }
-
     pub fn nix_path(self, nix_path: Option<String>) -> Self {
         Self { nix_path, ..self }
     }
@@ -234,8 +298,8 @@ impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> {
 }
 
 impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> {
-    pub fn source_map(&self) -> &SourceCode {
-        &self.source_map
+    pub fn source_map(&mut self) -> &SourceCode {
+        self.source_map.get_or_insert_with(SourceCode::default)
     }
 }
 
@@ -255,7 +319,9 @@ impl<'co, 'ro, 'env> EvaluationBuilder<'co, 'ro, 'env, Box<dyn EvalIO>> {
     pub fn enable_impure(mut self, io: Option<Box<dyn EvalIO>>) -> Self {
         self.io_handle = io.unwrap_or_else(|| Box::new(StdIO) as Box<dyn EvalIO>);
         self.enable_import = true;
-        self.builtins.extend(builtins::impure_builtins());
+        self.builtins_mut()
+            .builtins
+            .extend(builtins::impure_builtins());
 
         // Make `NIX_PATH` resolutions work by default, unless the
         // user already overrode this with something else.
@@ -332,9 +398,26 @@ pub struct EvaluationResult {
 }
 
 impl<'co, 'ro, 'env, IO> Evaluation<'co, 'ro, 'env, IO> {
+    /// Make a new [builder][] for configuring an evaluation
+    ///
+    /// [builder]: EvaluationBuilder
     pub fn builder(io_handle: IO) -> EvaluationBuilder<'co, 'ro, 'env, IO> {
         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, Box<dyn EvalIO>> {
@@ -352,12 +435,6 @@ impl<'co, 'ro, 'env, IO> Evaluation<'co, 'ro, 'env, IO>
 where
     IO: AsRef<dyn EvalIO> + 'static,
 {
-    /// Clone the reference to the contained source code map. This is used after
-    /// an evaluation for pretty error printing.
-    pub fn source_map(&self) -> SourceCode {
-        self.source_map.clone()
-    }
-
     /// 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)
diff --git a/tvix/utils.nix b/tvix/utils.nix
index 7adf8fe99733..1548d4de05b6 100644
--- a/tvix/utils.nix
+++ b/tvix/utils.nix
@@ -78,7 +78,10 @@
       };
 
       tvix-cli = prev: {
-        src = depot.tvix.utils.filterRustCrateSrc { root = prev.src.origSrc; };
+        src = depot.tvix.utils.filterRustCrateSrc rec {
+          root = prev.src.origSrc;
+          extraFileset = root + "/tests";
+        };
         buildInputs = lib.optional pkgs.stdenv.isDarwin commonDarwinDeps;
       };