about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--tvix/cli/src/main.rs43
-rw-r--r--tvix/eval/benches/eval.rs4
-rw-r--r--tvix/eval/src/lib.rs259
-rw-r--r--tvix/eval/src/tests/nix_tests.rs13
-rw-r--r--tvix/eval/src/tests/one_offs.rs9
-rw-r--r--tvix/eval/tests/nix_oracle.rs12
-rw-r--r--tvix/glue/benches/eval.rs21
-rw-r--r--tvix/glue/src/builtins/mod.rs41
-rw-r--r--tvix/glue/src/lib.rs16
-rw-r--r--tvix/glue/src/tests/mod.rs22
-rw-r--r--tvix/glue/src/tvix_store_io.rs10
-rw-r--r--tvix/serde/examples/nixpkgs.rs4
-rw-r--r--tvix/serde/src/de.rs14
-rw-r--r--tvix/serde/src/de_tests.rs4
-rw-r--r--web/tvixbolt/src/lib.rs9
15 files changed, 326 insertions, 155 deletions
diff --git a/tvix/cli/src/main.rs b/tvix/cli/src/main.rs
index c4c70d2a1777..2f7dc58a23aa 100644
--- a/tvix/cli/src/main.rs
+++ b/tvix/cli/src/main.rs
@@ -167,26 +167,25 @@ fn evaluate(
     span.pb_set_style(&tvix_tracing::PB_SPINNER_STYLE);
     span.pb_set_message("Setting up evaluator…");
 
-    let mut eval = tvix_eval::Evaluation::new(
-        Box::new(TvixIO::new(tvix_store_io.clone() as Rc<dyn EvalIO>)) as Box<dyn EvalIO>,
-        true,
-    );
-    eval.strict = args.strict;
-    eval.builtins.extend(impure_builtins());
-    if let Some(env) = env {
-        eval.env = Some(env);
-    }
-    add_derivation_builtins(&mut eval, Rc::clone(&tvix_store_io));
-    add_fetcher_builtins(&mut eval, Rc::clone(&tvix_store_io));
-    add_import_builtins(&mut eval, tvix_store_io);
-    configure_nix_path(&mut eval, &args.nix_search_path);
-
-    let source_map = eval.source_map();
+    let mut eval_builder = tvix_eval::Evaluation::builder(Box::new(TvixIO::new(
+        tvix_store_io.clone() as Rc<dyn EvalIO>,
+    )) 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);
+
+    let source_map = eval_builder.source_map().clone();
     let result = {
         let mut compiler_observer =
             DisassemblingObserver::new(source_map.clone(), std::io::stderr());
         if args.dump_bytecode {
-            eval.compiler_observer = Some(&mut compiler_observer);
+            eval_builder.set_compiler_observer(Some(&mut compiler_observer));
         }
 
         let mut runtime_observer = TracingObserver::new(std::io::stderr());
@@ -194,10 +193,12 @@ fn evaluate(
             if args.trace_runtime_timing {
                 runtime_observer.enable_timing()
             }
-            eval.runtime_observer = Some(&mut runtime_observer);
+            eval_builder.set_runtime_observer(Some(&mut runtime_observer));
         }
 
         span.pb_set_message("Evaluating…");
+
+        let eval = eval_builder.build();
         eval.evaluate(code, path)
     };
 
@@ -262,21 +263,21 @@ fn interpret(
 /// Interpret the given code snippet, but only run the Tvix compiler
 /// on it and return errors and warnings.
 fn lint(code: &str, path: Option<PathBuf>, args: &Args) -> bool {
-    let mut eval = tvix_eval::Evaluation::new_impure();
-    eval.strict = args.strict;
+    let mut eval_builder = tvix_eval::Evaluation::builder_impure().with_strict(args.strict);
 
-    let source_map = eval.source_map();
+    let source_map = eval_builder.source_map().clone();
 
     let mut compiler_observer = DisassemblingObserver::new(source_map.clone(), std::io::stderr());
 
     if args.dump_bytecode {
-        eval.compiler_observer = Some(&mut compiler_observer);
+        eval_builder.set_compiler_observer(Some(&mut compiler_observer));
     }
 
     if args.trace_runtime {
         eprintln!("warning: --trace-runtime has no effect with --compile-only!");
     }
 
+    let eval = eval_builder.build();
     let result = eval.compile_only(code, path);
 
     if args.display_ast {
diff --git a/tvix/eval/benches/eval.rs b/tvix/eval/benches/eval.rs
index 1333f5018cb3..36ca310f4465 100644
--- a/tvix/eval/benches/eval.rs
+++ b/tvix/eval/benches/eval.rs
@@ -8,7 +8,9 @@ use tikv_jemallocator::Jemalloc;
 static GLOBAL: Jemalloc = Jemalloc;
 
 fn interpret(code: &str) {
-    tvix_eval::Evaluation::new_pure().evaluate(code, None);
+    tvix_eval::Evaluation::builder_pure()
+        .build()
+        .evaluate(code, None);
 }
 
 fn eval_literals(c: &mut Criterion) {
diff --git a/tvix/eval/src/lib.rs b/tvix/eval/src/lib.rs
index 97690d13610e..bfdd30154acf 100644
--- a/tvix/eval/src/lib.rs
+++ b/tvix/eval/src/lib.rs
@@ -64,6 +64,192 @@ pub use crate::value::{Builtin, CoercionKind, NixAttrs, NixList, NixString, Valu
 #[cfg(feature = "impure")]
 pub use crate::io::StdIO;
 
+/// Builder for building an [`Evaluation`].
+///
+/// Construct an [`EvaluationBuilder`] by calling one of:
+///
+/// - [`Evaluation::builder`] / [`EvaluationBuilder::new`]
+/// - [`Evaluation::builder_impure`] [`EvaluationBuilder::new_impure`]
+/// - [`Evaluation::builder_pure`] [`EvaluationBuilder::new_pure`]
+///
+/// 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)>,
+    env: Option<&'env HashMap<SmolStr, Value>>,
+    io_handle: IO,
+    enable_import: bool,
+    strict: bool,
+    nix_path: Option<String>,
+    compiler_observer: Option<&'co mut dyn CompilerObserver>,
+    runtime_observer: Option<&'ro mut dyn RuntimeObserver>,
+}
+
+// NOTE(aspen): The methods here are intentionally incomplete; feel free to add new ones (ideally
+// with similar naming conventions to the ones already present) but don't expose fields publically!
+impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> {
+    pub fn new(io_handle: IO) -> Self {
+        let mut builtins = builtins::pure_builtins();
+        builtins.extend(builtins::placeholders()); // these are temporary
+
+        Self {
+            source_map: SourceCode::default(),
+            enable_import: false,
+            io_handle,
+            builtins,
+            src_builtins: vec![],
+            env: None,
+            strict: false,
+            nix_path: None,
+            compiler_observer: None,
+            runtime_observer: None,
+        }
+    }
+
+    pub fn build(self) -> Evaluation<'co, 'ro, 'env, IO> {
+        Evaluation {
+            source_map: self.source_map,
+            builtins: self.builtins,
+            src_builtins: self.src_builtins,
+            env: self.env,
+            io_handle: self.io_handle,
+            enable_import: self.enable_import,
+            strict: self.strict,
+            nix_path: self.nix_path,
+            compiler_observer: self.compiler_observer,
+            runtime_observer: self.runtime_observer,
+        }
+    }
+
+    pub fn io_handle<IO2>(self, io_handle: IO2) -> EvaluationBuilder<'co, 'ro, 'env, IO2> {
+        EvaluationBuilder {
+            io_handle,
+            source_map: self.source_map,
+            builtins: self.builtins,
+            src_builtins: self.src_builtins,
+            env: self.env,
+            enable_import: self.enable_import,
+            strict: self.strict,
+            nix_path: self.nix_path,
+            compiler_observer: self.compiler_observer,
+            runtime_observer: self.runtime_observer,
+        }
+    }
+
+    pub fn with_enable_import(self, enable_import: bool) -> Self {
+        Self {
+            enable_import,
+            ..self
+        }
+    }
+
+    pub fn disable_import(self) -> Self {
+        self.with_enable_import(false)
+    }
+
+    pub fn enable_import(self) -> Self {
+        self.with_enable_import(true)
+    }
+
+    pub fn add_builtins<I>(mut self, builtins: I) -> Self
+    where
+        I: IntoIterator<Item = (&'static str, Value)>,
+    {
+        self.builtins.extend(builtins);
+        self
+    }
+
+    pub fn with_strict(self, strict: bool) -> Self {
+        Self { strict, ..self }
+    }
+
+    pub fn strict(self) -> Self {
+        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 }
+    }
+
+    pub fn env(self, env: Option<&'env HashMap<SmolStr, Value>>) -> Self {
+        Self { env, ..self }
+    }
+
+    pub fn compiler_observer(
+        self,
+        compiler_observer: Option<&'co mut dyn CompilerObserver>,
+    ) -> Self {
+        Self {
+            compiler_observer,
+            ..self
+        }
+    }
+
+    pub fn set_compiler_observer(
+        &mut self,
+        compiler_observer: Option<&'co mut dyn CompilerObserver>,
+    ) {
+        self.compiler_observer = compiler_observer;
+    }
+
+    pub fn runtime_observer(self, runtime_observer: Option<&'ro mut dyn RuntimeObserver>) -> Self {
+        Self {
+            runtime_observer,
+            ..self
+        }
+    }
+
+    pub fn set_runtime_observer(&mut self, runtime_observer: Option<&'ro mut dyn RuntimeObserver>) {
+        self.runtime_observer = runtime_observer;
+    }
+}
+
+impl<'co, 'ro, 'env, IO> EvaluationBuilder<'co, 'ro, 'env, IO> {
+    pub fn source_map(&self) -> &SourceCode {
+        &self.source_map
+    }
+}
+
+impl<'co, 'ro, 'env> EvaluationBuilder<'co, 'ro, 'env, Box<dyn EvalIO>> {
+    /// Initialize an `Evaluation`, without the import statement available, and
+    /// all IO operations stubbed out.
+    pub fn new_pure() -> Self {
+        Self::new(Box::new(DummyIO) as Box<dyn EvalIO>).with_enable_import(false)
+    }
+
+    #[cfg(feature = "impure")]
+    /// Configure an `Evaluation` to have impure features available
+    /// with the given I/O implementation.
+    ///
+    /// If no I/O implementation is supplied, [`StdIO`] is used by
+    /// default.
+    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());
+
+        // Make `NIX_PATH` resolutions work by default, unless the
+        // user already overrode this with something else.
+        if self.nix_path.is_none() {
+            self.nix_path = std::env::var("NIX_PATH").ok();
+        }
+        self
+    }
+
+    #[cfg(feature = "impure")]
+    /// Initialise an `Evaluation`, with all impure features turned on by default.
+    pub fn new_impure() -> Self {
+        Self::new_pure().enable_impure(None)
+    }
+}
+
 /// An `Evaluation` represents how a piece of Nix code is evaluated. It can be
 /// instantiated and configured directly, or it can be accessed through the
 /// various simplified helper methods available below.
@@ -79,42 +265,42 @@ pub struct Evaluation<'co, 'ro, 'env, IO> {
     ///
     /// This defaults to all pure builtins. Users might want to add
     /// the set of impure builtins, or other custom builtins.
-    pub builtins: Vec<(&'static str, Value)>,
+    builtins: Vec<(&'static str, Value)>,
 
     /// Set of builtins that are implemented in Nix itself and should
     /// be compiled and inserted in the builtins set.
-    pub src_builtins: Vec<(&'static str, &'static str)>,
+    src_builtins: Vec<(&'static str, &'static str)>,
 
     /// Top-level variables to define in the evaluation
-    pub env: Option<&'env HashMap<SmolStr, Value>>,
+    env: Option<&'env HashMap<SmolStr, Value>>,
 
     /// Implementation of file-IO to use during evaluation, e.g. for
     /// impure builtins.
     ///
     /// Defaults to [`DummyIO`] if not set explicitly.
-    pub io_handle: IO,
+    io_handle: IO,
 
     /// Determines whether the `import` builtin should be made
     /// available. Note that this depends on the `io_handle` being
     /// able to read the files specified as arguments to `import`.
-    pub enable_import: bool,
+    enable_import: bool,
 
     /// Determines whether the returned value should be strictly
     /// evaluated, that is whether its list and attribute set elements
     /// should be forced recursively.
-    pub strict: bool,
+    strict: bool,
 
     /// (optional) Nix search path, e.g. the value of `NIX_PATH` used
     /// for resolving items on the search path (such as `<nixpkgs>`).
-    pub nix_path: Option<String>,
+    nix_path: Option<String>,
 
     /// (optional) compiler observer for reporting on compilation
     /// details, like the emitted bytecode.
-    pub compiler_observer: Option<&'co mut dyn CompilerObserver>,
+    compiler_observer: Option<&'co mut dyn CompilerObserver>,
 
     /// (optional) runtime observer, for reporting on execution steps
     /// of Nix code.
-    pub runtime_observer: Option<&'ro mut dyn RuntimeObserver>,
+    runtime_observer: Option<&'ro mut dyn RuntimeObserver>,
 }
 
 /// Result of evaluating a piece of Nix code. If evaluation succeeded, a value
@@ -136,61 +322,20 @@ pub struct EvaluationResult {
     pub expr: Option<rnix::ast::Expr>,
 }
 
-impl<'co, 'ro, 'env, IO> Evaluation<'co, 'ro, 'env, IO>
-where
-    IO: AsRef<dyn EvalIO> + 'static,
-{
-    /// Initialize an `Evaluation`.
-    pub fn new(io_handle: IO, enable_import: bool) -> Self {
-        let mut builtins = builtins::pure_builtins();
-        builtins.extend(builtins::placeholders()); // these are temporary
-
-        Self {
-            source_map: SourceCode::default(),
-            enable_import,
-            io_handle,
-            builtins,
-            src_builtins: vec![],
-            env: None,
-            strict: false,
-            nix_path: None,
-            compiler_observer: None,
-            runtime_observer: None,
-        }
+impl<'co, 'ro, 'env, IO> Evaluation<'co, 'ro, 'env, IO> {
+    pub fn builder(io_handle: IO) -> EvaluationBuilder<'co, 'ro, 'env, IO> {
+        EvaluationBuilder::new(io_handle)
     }
 }
 
 impl<'co, 'ro, 'env> Evaluation<'co, 'ro, 'env, Box<dyn EvalIO>> {
-    /// Initialize an `Evaluation`, without the import statement available, and
-    /// all IO operations stubbed out.
-    pub fn new_pure() -> Self {
-        Self::new(Box::new(DummyIO) as Box<dyn EvalIO>, false)
-    }
-
     #[cfg(feature = "impure")]
-    /// Configure an `Evaluation` to have impure features available
-    /// with the given I/O implementation.
-    ///
-    /// If no I/O implementation is supplied, [`StdIO`] is used by
-    /// default.
-    pub fn enable_impure(&mut self, io: Option<Box<dyn EvalIO>>) {
-        self.io_handle = io.unwrap_or_else(|| Box::new(StdIO) as Box<dyn EvalIO>);
-        self.enable_import = true;
-        self.builtins.extend(builtins::impure_builtins());
-
-        // Make `NIX_PATH` resolutions work by default, unless the
-        // user already overrode this with something else.
-        if self.nix_path.is_none() {
-            self.nix_path = std::env::var("NIX_PATH").ok();
-        }
+    pub fn builder_impure() -> EvaluationBuilder<'co, 'ro, 'env, Box<dyn EvalIO>> {
+        EvaluationBuilder::new_impure()
     }
 
-    #[cfg(feature = "impure")]
-    /// Initialise an `Evaluation`, with all impure features turned on by default.
-    pub fn new_impure() -> Self {
-        let mut eval = Self::new_pure();
-        eval.enable_impure(None);
-        eval
+    pub fn builder_pure() -> EvaluationBuilder<'co, 'ro, 'env, Box<dyn EvalIO>> {
+        EvaluationBuilder::new_pure()
     }
 }
 
diff --git a/tvix/eval/src/tests/nix_tests.rs b/tvix/eval/src/tests/nix_tests.rs
index 17968e4bdb09..cdaed193f206 100644
--- a/tvix/eval/src/tests/nix_tests.rs
+++ b/tvix/eval/src/tests/nix_tests.rs
@@ -48,9 +48,10 @@ fn eval_test(code_path: PathBuf, expect_success: bool) {
 
     let code = std::fs::read_to_string(&code_path).expect("should be able to read test code");
 
-    let mut eval = crate::Evaluation::new_impure();
-    eval.strict = true;
-    eval.builtins.extend(mock_builtins::builtins());
+    let eval = crate::Evaluation::builder_impure()
+        .strict()
+        .add_builtins(mock_builtins::builtins())
+        .build();
 
     let result = eval.evaluate(code, Some(code_path.clone()));
     let failed = match result.value {
@@ -128,8 +129,10 @@ fn identity(#[files("src/tests/tvix_tests/identity-*.nix")] code_path: PathBuf)
 
     let code = std::fs::read_to_string(code_path).expect("should be able to read test code");
 
-    let mut eval = crate::Evaluation::new(Box::new(crate::StdIO) as Box<dyn EvalIO>, false);
-    eval.strict = true;
+    let eval = crate::Evaluation::builder(Box::new(crate::StdIO) as Box<dyn EvalIO>)
+        .disable_import()
+        .strict()
+        .build();
 
     let result = eval.evaluate(&code, None);
     assert!(
diff --git a/tvix/eval/src/tests/one_offs.rs b/tvix/eval/src/tests/one_offs.rs
index 21e9144baf64..49ed713fc96c 100644
--- a/tvix/eval/src/tests/one_offs.rs
+++ b/tvix/eval/src/tests/one_offs.rs
@@ -5,8 +5,9 @@ fn test_source_builtin() {
     // Test an evaluation with a source-only builtin. The test ensures
     // that the artificially constructed thunking is correct.
 
-    let mut eval = Evaluation::new_pure();
-    eval.src_builtins.push(("testSourceBuiltin", "42"));
+    let eval = Evaluation::builder_pure()
+        .add_src_builtin("testSourceBuiltin", "42")
+        .build();
 
     let result = eval.evaluate("builtins.testSourceBuiltin", None);
     assert!(
@@ -25,7 +26,9 @@ fn test_source_builtin() {
 
 #[test]
 fn skip_broken_bytecode() {
-    let result = Evaluation::new_pure().evaluate(/* code = */ "x", None);
+    let result = Evaluation::builder_pure()
+        .build()
+        .evaluate(/* code = */ "x", None);
 
     assert_eq!(result.errors.len(), 1);
 
diff --git a/tvix/eval/tests/nix_oracle.rs b/tvix/eval/tests/nix_oracle.rs
index 3d3abc73635f..cc2a7b519e8f 100644
--- a/tvix/eval/tests/nix_oracle.rs
+++ b/tvix/eval/tests/nix_oracle.rs
@@ -58,10 +58,16 @@ fn nix_eval(expr: &str, strictness: Strictness) -> String {
 #[track_caller]
 #[cfg(feature = "impure")]
 fn compare_eval(expr: &str, strictness: Strictness) {
+    use tvix_eval::EvalIO;
+
     let nix_result = nix_eval(expr, strictness);
-    let mut eval = tvix_eval::Evaluation::new_pure();
-    eval.strict = matches!(strictness, Strictness::Strict);
-    eval.io_handle = Box::new(tvix_eval::StdIO);
+    let mut eval_builder = tvix_eval::Evaluation::builder_pure();
+    if matches!(strictness, Strictness::Strict) {
+        eval_builder = eval_builder.strict();
+    }
+    let eval = eval_builder
+        .io_handle(Box::new(tvix_eval::StdIO) as Box<dyn EvalIO>)
+        .build();
 
     let tvix_result = eval
         .evaluate(expr, None)
diff --git a/tvix/glue/benches/eval.rs b/tvix/glue/benches/eval.rs
index 9e0154cad787..55f37f2d58ed 100644
--- a/tvix/glue/benches/eval.rs
+++ b/tvix/glue/benches/eval.rs
@@ -40,22 +40,23 @@ fn interpret(code: &str) {
         TOKIO_RUNTIME.handle().clone(),
     ));
 
-    let mut eval = tvix_eval::Evaluation::new(
-        Box::new(TvixIO::new(tvix_store_io.clone() as Rc<dyn EvalIO>)) as Box<dyn EvalIO>,
-        true,
-    );
+    let mut eval_builder = tvix_eval::Evaluation::builder(Box::new(TvixIO::new(
+        tvix_store_io.clone() as Rc<dyn EvalIO>,
+    )) as Box<dyn EvalIO>)
+    .enable_import()
+    .add_builtins(impure_builtins());
 
-    eval.builtins.extend(impure_builtins());
-    add_derivation_builtins(&mut eval, Rc::clone(&tvix_store_io));
-    add_fetcher_builtins(&mut eval, Rc::clone(&tvix_store_io));
-    add_import_builtins(&mut eval, tvix_store_io);
-    configure_nix_path(
-        &mut eval,
+    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,
         // The benchmark requires TVIX_BENCH_NIX_PATH to be set, so barf out
         // early, rather than benchmarking tvix returning an error.
         &Some(env::var("TVIX_BENCH_NIX_PATH").expect("TVIX_BENCH_NIX_PATH must be set")),
     );
 
+    let eval = eval_builder.build();
     let result = eval.evaluate(code, None);
 
     assert!(result.errors.is_empty());
diff --git a/tvix/glue/src/builtins/mod.rs b/tvix/glue/src/builtins/mod.rs
index 3d6263286dc4..e1c20e461ffc 100644
--- a/tvix/glue/src/builtins/mod.rs
+++ b/tvix/glue/src/builtins/mod.rs
@@ -18,13 +18,14 @@ pub use errors::{DerivationError, FetcherError, ImportError};
 ///
 /// As they need to interact with `known_paths`, we also need to pass in
 /// `known_paths`.
-pub fn add_derivation_builtins<IO>(eval: &mut tvix_eval::Evaluation<IO>, io: Rc<TvixStoreIO>) {
-    eval.builtins
-        .extend(derivation::derivation_builtins::builtins(Rc::clone(&io)));
-
-    // Add the actual `builtins.derivation` from compiled Nix code
-    eval.src_builtins
-        .push(("derivation", include_str!("derivation.nix")));
+pub fn add_derivation_builtins<'co, 'ro, 'env, IO>(
+    eval_builder: tvix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
+    io: Rc<TvixStoreIO>,
+) -> tvix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
+    eval_builder
+        .add_builtins(derivation::derivation_builtins::builtins(Rc::clone(&io)))
+        // Add the actual `builtins.derivation` from compiled Nix code
+        .add_src_builtin("derivation", include_str!("derivation.nix"))
 }
 
 /// Adds fetcher builtins to the passed [tvix_eval::Evaluation]:
@@ -32,9 +33,11 @@ pub fn add_derivation_builtins<IO>(eval: &mut tvix_eval::Evaluation<IO>, io: Rc<
 /// * `fetchurl`
 /// * `fetchTarball`
 /// * `fetchGit`
-pub fn add_fetcher_builtins<IO>(eval: &mut tvix_eval::Evaluation<IO>, io: Rc<TvixStoreIO>) {
-    eval.builtins
-        .extend(fetchers::fetcher_builtins::builtins(Rc::clone(&io)));
+pub fn add_fetcher_builtins<'co, 'ro, 'env, IO>(
+    eval_builder: tvix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
+    io: Rc<TvixStoreIO>,
+) -> tvix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
+    eval_builder.add_builtins(fetchers::fetcher_builtins::builtins(Rc::clone(&io)))
 }
 
 /// Adds import-related builtins to the passed [tvix_eval::Evaluation].
@@ -42,10 +45,12 @@ pub fn add_fetcher_builtins<IO>(eval: &mut tvix_eval::Evaluation<IO>, io: Rc<Tvi
 /// These are `filterSource` and `path`
 ///
 /// As they need to interact with the store implementation, we pass [`TvixStoreIO`].
-pub fn add_import_builtins<IO>(eval: &mut tvix_eval::Evaluation<IO>, io: Rc<TvixStoreIO>) {
-    eval.builtins.extend(import::import_builtins(io));
-
+pub fn add_import_builtins<'co, 'ro, 'env, IO>(
+    eval_builder: tvix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
+    io: Rc<TvixStoreIO>,
+) -> tvix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
     // TODO(raitobezarius): evaluate expressing filterSource as Nix code using path (b/372)
+    eval_builder.add_builtins(import::import_builtins(io))
 }
 
 #[cfg(test)]
@@ -81,11 +86,11 @@ mod tests {
             runtime.handle().clone(),
         ));
 
-        let mut eval = tvix_eval::Evaluation::new(io.clone() as Rc<dyn EvalIO>, false);
-
-        add_derivation_builtins(&mut eval, Rc::clone(&io));
-        add_fetcher_builtins(&mut eval, Rc::clone(&io));
-        add_import_builtins(&mut eval, io);
+        let mut eval_builder = tvix_eval::Evaluation::builder(io.clone() as Rc<dyn EvalIO>);
+        eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&io));
+        eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&io));
+        eval_builder = add_import_builtins(eval_builder, io);
+        let eval = eval_builder.build();
 
         // run the evaluation itself.
         eval.evaluate(str, None)
diff --git a/tvix/glue/src/lib.rs b/tvix/glue/src/lib.rs
index a5dbdb8742fc..f6f99f3c32a3 100644
--- a/tvix/glue/src/lib.rs
+++ b/tvix/glue/src/lib.rs
@@ -14,12 +14,14 @@ mod tests;
 /// Tell the Evaluator to resolve `<nix>` to the path `/__corepkgs__`,
 /// which has special handling in [tvix_io::TvixIO].
 /// This is used in nixpkgs to import `fetchurl.nix` from `<nix>`.
-pub fn configure_nix_path<IO>(
-    eval: &mut tvix_eval::Evaluation<IO>,
+pub fn configure_nix_path<'co, 'ro, 'env, IO>(
+    eval_builder: tvix_eval::EvaluationBuilder<'co, 'ro, 'env, IO>,
     nix_search_path: &Option<String>,
-) {
-    eval.nix_path = nix_search_path
-        .as_ref()
-        .map(|p| format!("nix=/__corepkgs__:{}", p))
-        .or_else(|| Some("nix=/__corepkgs__".to_string()));
+) -> tvix_eval::EvaluationBuilder<'co, 'ro, 'env, IO> {
+    eval_builder.nix_path(
+        nix_search_path
+            .as_ref()
+            .map(|p| format!("nix=/__corepkgs__:{}", p))
+            .or_else(|| Some("nix=/__corepkgs__".to_string())),
+    )
 }
diff --git a/tvix/glue/src/tests/mod.rs b/tvix/glue/src/tests/mod.rs
index 5b474be73657..142ae1a07714 100644
--- a/tvix/glue/src/tests/mod.rs
+++ b/tvix/glue/src/tests/mod.rs
@@ -47,16 +47,18 @@ fn eval_test(code_path: PathBuf, expect_success: bool) {
         tokio_runtime.handle().clone(),
     ));
     // Wrap with TvixIO, so <nix/fetchurl.nix can be imported.
-    let mut eval = tvix_eval::Evaluation::new(
-        Box::new(TvixIO::new(tvix_store_io.clone() as Rc<dyn EvalIO>)) as Box<dyn EvalIO>,
-        true,
-    );
-
-    eval.strict = true;
-    add_derivation_builtins(&mut eval, tvix_store_io.clone());
-    add_fetcher_builtins(&mut eval, tvix_store_io.clone());
-    add_import_builtins(&mut eval, tvix_store_io.clone());
-    configure_nix_path(&mut eval, &None);
+    let mut eval_builder = tvix_eval::Evaluation::builder(Box::new(TvixIO::new(
+        tvix_store_io.clone() as Rc<dyn EvalIO>,
+    )) as Box<dyn EvalIO>)
+    .enable_import()
+    .strict();
+
+    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, &None);
+
+    let eval = eval_builder.build();
 
     let result = eval.evaluate(code, Some(code_path.clone()));
     let failed = match result.value {
diff --git a/tvix/glue/src/tvix_store_io.rs b/tvix/glue/src/tvix_store_io.rs
index 4e5488067f8f..e8c09af0ec4c 100644
--- a/tvix/glue/src/tvix_store_io.rs
+++ b/tvix/glue/src/tvix_store_io.rs
@@ -653,11 +653,13 @@ mod tests {
             Arc::<DummyBuildService>::default(),
             tokio_runtime.handle().clone(),
         ));
-        let mut eval = tvix_eval::Evaluation::new(io.clone() as Rc<dyn EvalIO>, true);
 
-        add_derivation_builtins(&mut eval, io.clone());
-        add_fetcher_builtins(&mut eval, io.clone());
-        add_import_builtins(&mut eval, io);
+        let mut eval_builder =
+            tvix_eval::Evaluation::builder(io.clone() as Rc<dyn EvalIO>).enable_import();
+        eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&io));
+        eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&io));
+        eval_builder = add_import_builtins(eval_builder, io);
+        let eval = eval_builder.build();
 
         // run the evaluation itself.
         eval.evaluate(str, None)
diff --git a/tvix/serde/examples/nixpkgs.rs b/tvix/serde/examples/nixpkgs.rs
index ad8c4160b5d0..e3c589dba118 100644
--- a/tvix/serde/examples/nixpkgs.rs
+++ b/tvix/serde/examples/nixpkgs.rs
@@ -23,8 +23,8 @@ fn main() {
     }
     "#;
 
-    let result = tvix_serde::from_str_with_config::<Config, _>(code, |eval| {
-        eval.enable_impure(None);
+    let result = tvix_serde::from_str_with_config::<Config, _>(code, |eval_builder| {
+        eval_builder.enable_impure(None)
     });
 
     match result {
diff --git a/tvix/serde/src/de.rs b/tvix/serde/src/de.rs
index 428b9e2e8114..88cb46d618ce 100644
--- a/tvix/serde/src/de.rs
+++ b/tvix/serde/src/de.rs
@@ -3,8 +3,7 @@
 use bstr::ByteSlice;
 use serde::de::value::{MapDeserializer, SeqDeserializer};
 use serde::de::{self, EnumAccess, VariantAccess};
-pub use tvix_eval::Evaluation;
-use tvix_eval::{EvalIO, Value};
+use tvix_eval::{EvalIO, EvaluationBuilder, Value};
 
 use crate::error::Error;
 
@@ -36,7 +35,7 @@ pub fn from_str<'code, T>(src: &'code str) -> Result<T, Error>
 where
     T: serde::Deserialize<'code>,
 {
-    from_str_with_config(src, |_| /* no extra config */ ())
+    from_str_with_config(src, |b| /* no extra config */ b)
 }
 
 /// Evaluate the Nix code in `src`, with extra configuration for the
@@ -44,13 +43,12 @@ where
 pub fn from_str_with_config<'code, T, F>(src: &'code str, config: F) -> Result<T, Error>
 where
     T: serde::Deserialize<'code>,
-    F: FnOnce(&mut Evaluation<Box<dyn EvalIO>>),
+    F: for<'co, 'ro, 'env> FnOnce(
+        EvaluationBuilder<'co, 'ro, 'env, Box<dyn EvalIO>>,
+    ) -> EvaluationBuilder<'co, 'ro, 'env, Box<dyn EvalIO>>,
 {
     // First step is to evaluate the Nix code ...
-    let mut eval = Evaluation::new_pure();
-    config(&mut eval);
-
-    eval.strict = true;
+    let eval = config(EvaluationBuilder::new_pure().strict()).build();
     let result = eval.evaluate(src, None);
 
     if !result.errors.is_empty() {
diff --git a/tvix/serde/src/de_tests.rs b/tvix/serde/src/de_tests.rs
index 1c3acd1c2f52..fea7c8290bd7 100644
--- a/tvix/serde/src/de_tests.rs
+++ b/tvix/serde/src/de_tests.rs
@@ -204,7 +204,7 @@ fn deserialize_enum_all() {
 fn deserialize_with_config() {
     let result: String = from_str_with_config("builtins.testWithConfig", |eval| {
         // Add a literal string builtin that just returns `"ok"`.
-        eval.src_builtins.push(("testWithConfig", "\"ok\""));
+        eval.add_src_builtin("testWithConfig", "\"ok\"")
     })
     .expect("should deserialize");
 
@@ -237,7 +237,7 @@ fn deserialize_with_extra_builtin() {
     let code = "builtins.prependHello \"world\"";
 
     let result: String = from_str_with_config(code, |eval| {
-        eval.builtins.append(&mut test_builtins::builtins());
+        eval.add_builtins(test_builtins::builtins())
     })
     .expect("should deserialize");
 
diff --git a/web/tvixbolt/src/lib.rs b/web/tvixbolt/src/lib.rs
index 1f47b0eaf59d..87627619d79d 100644
--- a/web/tvixbolt/src/lib.rs
+++ b/web/tvixbolt/src/lib.rs
@@ -265,18 +265,19 @@ fn eval(model: &Model) -> Output {
         return out;
     }
 
-    let mut eval = tvix_eval::Evaluation::new_pure();
-    let source = eval.source_map();
+    let mut eval_builder = tvix_eval::Evaluation::builder_pure();
+    let source = eval_builder.source_map().clone();
 
     let result = {
         let mut compiler_observer = DisassemblingObserver::new(source.clone(), &mut out.bytecode);
-        eval.compiler_observer = Some(&mut compiler_observer);
+        eval_builder.set_compiler_observer(Some(&mut compiler_observer));
 
         let mut runtime_observer = TracingObserver::new(&mut out.trace);
         if model.trace {
-            eval.runtime_observer = Some(&mut runtime_observer);
+            eval_builder.set_runtime_observer(Some(&mut runtime_observer));
         }
 
+        let eval = eval_builder.build();
         eval.evaluate(&model.code, Some("/nixbolt".into()))
     };