about summary refs log tree commit diff
path: root/tvix/eval/src/compiler
diff options
context:
space:
mode:
Diffstat (limited to 'tvix/eval/src/compiler')
-rw-r--r--tvix/eval/src/compiler/import.rs105
-rw-r--r--tvix/eval/src/compiler/mod.rs103
2 files changed, 187 insertions, 21 deletions
diff --git a/tvix/eval/src/compiler/import.rs b/tvix/eval/src/compiler/import.rs
new file mode 100644
index 000000000000..3a8847f2cbb4
--- /dev/null
+++ b/tvix/eval/src/compiler/import.rs
@@ -0,0 +1,105 @@
+//! This module implements the Nix language's `import` feature, which
+//! is exposed as a builtin in the Nix language.
+//!
+//! This is not a typical builtin, as it needs access to internal
+//! compiler and VM state (such as the [`crate::SourceCode`]
+//! instance, or observers).
+
+use std::rc::Weak;
+
+use crate::{
+    observer::NoOpObserver,
+    value::{Builtin, BuiltinArgument, Thunk},
+    vm::VM,
+    ErrorKind, SourceCode, Value,
+};
+
+use super::GlobalsMap;
+use crate::builtins::coerce_value_to_path;
+
+/// Constructs and inserts the `import` builtin. This builtin is special in that
+/// it needs to capture the [crate::SourceCode] structure to correctly track
+/// source code locations while invoking a compiler.
+// TODO: need to be able to pass through a CompilationObserver, too.
+// TODO: can the `SourceCode` come from the compiler?
+pub(super) fn builtins_import(globals: &Weak<GlobalsMap>, source: SourceCode) -> Builtin {
+    // This (very cheap, once-per-compiler-startup) clone exists
+    // solely in order to keep the borrow checker happy.  It
+    // resolves the tension between the requirements of
+    // Rc::new_cyclic() and Builtin::new()
+    let globals = globals.clone();
+
+    Builtin::new(
+        "import",
+        &[BuiltinArgument {
+            strict: true,
+            name: "path",
+        }],
+        None,
+        move |mut args: Vec<Value>, vm: &mut VM| {
+            let mut path = coerce_value_to_path(&args.pop().unwrap(), vm)?;
+            if path.is_dir() {
+                path.push("default.nix");
+            }
+
+            let current_span = vm.current_light_span();
+
+            if let Some(cached) = vm.import_cache.get(&path) {
+                return Ok(cached.clone());
+            }
+
+            let contents = vm.io().read_to_string(path.clone())?;
+
+            let parsed = rnix::ast::Root::parse(&contents);
+            let errors = parsed.errors();
+
+            let file = source.add_file(path.to_string_lossy().to_string(), contents);
+
+            if !errors.is_empty() {
+                return Err(ErrorKind::ImportParseError {
+                    path,
+                    file,
+                    errors: errors.to_vec(),
+                });
+            }
+
+            let result = crate::compiler::compile(
+                &parsed.tree().expr().unwrap(),
+                Some(path.clone()),
+                file,
+                // The VM must ensure that a strong reference to the
+                // globals outlives any self-references (which are
+                // weak) embedded within the globals.  If the
+                // expect() below panics, it means that did not
+                // happen.
+                globals
+                    .upgrade()
+                    .expect("globals dropped while still in use"),
+                &mut NoOpObserver::default(),
+            )
+            .map_err(|err| ErrorKind::ImportCompilerError {
+                path: path.clone(),
+                errors: vec![err],
+            })?;
+
+            if !result.errors.is_empty() {
+                return Err(ErrorKind::ImportCompilerError {
+                    path,
+                    errors: result.errors,
+                });
+            }
+
+            // Compilation succeeded, we can construct a thunk from whatever it spat
+            // out and return that.
+            let res = Value::Thunk(Thunk::new_suspended(result.lambda, current_span));
+
+            vm.import_cache.insert(path, res.clone());
+
+            for warning in result.warnings {
+                vm.push_warning(warning);
+            }
+
+            Ok(res)
+        },
+    )
+}
diff --git a/tvix/eval/src/compiler/mod.rs b/tvix/eval/src/compiler/mod.rs
index 12fd269c2f9b..4a44f95691c0 100644
--- a/tvix/eval/src/compiler/mod.rs
+++ b/tvix/eval/src/compiler/mod.rs
@@ -14,6 +14,7 @@
 //! mistakes early during development.
 
 mod bindings;
+mod import;
 mod scope;
 
 use codemap::Span;
@@ -30,8 +31,9 @@ use crate::observer::CompilerObserver;
 use crate::opcode::{CodeIdx, Count, JumpOffset, OpCode, UpvalueIdx};
 use crate::spans::LightSpan;
 use crate::spans::ToSpan;
-use crate::value::{Closure, Formals, Lambda, Thunk, Value};
+use crate::value::{Closure, Formals, Lambda, NixAttrs, Thunk, Value};
 use crate::warnings::{EvalWarning, WarningKind};
+use crate::SourceCode;
 
 use self::scope::{LocalIdx, LocalPosition, Scope, Upvalue, UpvalueKind};
 
@@ -73,17 +75,38 @@ impl LambdaCtx {
     }
 }
 
+/// The type of a global as used inside of the compiler. Differs from
+/// Nix's own notion of "builtins" in that it can emit arbitrary code.
+/// Nix's builtins are wrapped inside of this type.
+pub type Global = Rc<dyn Fn(&mut Compiler, Span)>;
+
 /// The map of globally available functions that should implicitly
 /// be resolvable in the global scope.
-pub type GlobalsMap = HashMap<&'static str, Rc<dyn Fn(&mut Compiler, Span)>>;
-
-/// Functions with this type are used to construct a
-/// self-referential `builtins` object; it takes a weak reference to
-/// its own result, similar to how nixpkgs' overlays work.
-/// Rc::new_cyclic() is what "ties the knot".  The heap allocation
-/// (Box) and vtable (dyn) do not impair runtime or compile-time
-/// performance; they exist only during compiler startup.
-pub type GlobalsMapFunc = Box<dyn FnOnce(&Weak<GlobalsMap>) -> GlobalsMap>;
+type GlobalsMap = HashMap<&'static str, Rc<dyn Fn(&mut Compiler, Span)>>;
+
+/// Set of builtins that (if they exist) should be made available in
+/// the global scope, meaning that they can be accessed not just
+/// through `builtins.<name>`, but directly as `<name>`. This is not
+/// configurable, it is based on what Nix 2.3 exposed.
+const GLOBAL_BUILTINS: &'static [&'static str] = &[
+    "abort",
+    "baseNameOf",
+    "derivation",
+    "derivationStrict",
+    "dirOf",
+    "fetchGit",
+    "fetchMercurial",
+    "fetchTarball",
+    "fromTOML",
+    "import",
+    "isNull",
+    "map",
+    "placeholder",
+    "removeAttrs",
+    "scopedImport",
+    "throw",
+    "toString",
+];
 
 pub struct Compiler<'observer> {
     contexts: Vec<LambdaCtx>,
@@ -1183,19 +1206,57 @@ fn optimise_tail_call(chunk: &mut Chunk) {
     }
 }
 
-/// Prepare the full set of globals from additional globals supplied
-/// by the caller of the compiler, as well as the built-in globals
-/// that are always part of the language.  This also "ties the knot"
-/// required in order for import to have a reference cycle back to
-/// the globals.
+/// Prepare the full set of globals available in evaluated code. These
+/// are constructed from the set of builtins supplied by the caller,
+/// which are made available globally under the `builtins` identifier.
+///
+/// A subset of builtins (specified by [`GLOBAL_BUILTINS`]) is
+/// available globally *iff* they are set.
 ///
-/// Note that all builtin functions are *not* considered part of the
-/// language in this sense and MUST be supplied as additional global
-/// values, including the `builtins` set itself.
-pub fn prepare_globals(additional: GlobalsMapFunc) -> Rc<GlobalsMap> {
-    Rc::new_cyclic(Box::new(|weak: &Weak<GlobalsMap>| {
-        let mut globals = additional(weak);
+/// Optionally adds the `import` feature if desired by the caller.
+pub fn prepare_globals(
+    builtins: Vec<(&'static str, Value)>,
+    source: SourceCode,
+    enable_import: bool,
+) -> Rc<GlobalsMap> {
+    Rc::new_cyclic(Box::new(move |weak: &Weak<GlobalsMap>| {
+        // First step is to construct the builtins themselves as
+        // `NixAttrs`.
+        let mut builtins_under_construction: HashMap<&'static str, Value> =
+            HashMap::from_iter(builtins.into_iter());
+
+        // At this point, optionally insert `import` if enabled. To
+        // "tie the knot" of `import` needing the full set of globals
+        // to instantiate its compiler, the `Weak` reference is passed
+        // here.
+        if enable_import {
+            let import = Value::Builtin(import::builtins_import(weak, source));
+            builtins_under_construction.insert("import", import);
+        }
+
+        // Next, the actual map of globals is constructed and
+        // populated with (copies) of the values that should be
+        // available in the global scope (see [`GLOBAL_BUILTINS`]).
+        let mut globals: GlobalsMap = HashMap::new();
+
+        for global in GLOBAL_BUILTINS {
+            if let Some(builtin) = builtins_under_construction.get(global).cloned() {
+                let global_builtin: Global =
+                    Rc::new(move |c, s| c.emit_constant(builtin.clone(), &s));
+                globals.insert(global, global_builtin);
+            }
+        }
+
+        // This is followed by the actual `builtins` attribute set
+        // being constructed and inserted in the global scope.
+        let builtins_set =
+            Value::attrs(NixAttrs::from_iter(builtins_under_construction.into_iter()));
+        globals.insert(
+            "builtins",
+            Rc::new(move |c, s| c.emit_constant(builtins_set.clone(), &s)),
+        );
 
+        // Finally insert the compiler-internal "magic" builtins for top-level values.
         globals.insert(
             "true",
             Rc::new(|compiler, span| {