snix/tvix/eval/src/compiler/import.rs
Aspen Smith ac3d717944 feat(tvix/eval): Allow passing in an env to evaluation
Allow passing in a top-level env, a map from name to value, to
evaluation. The intent is to support bound identifiers in the REPL just
like upstream nix does.

Getting this working involves mucking around a bit with internals - most
notably, locals now only optionally have a Span (since locals don't have
an easy span we can use) - and getting that working requires propagating
some minor hacks to places where we currently *need* a span (and which
would require too much changing now to make spans optional; my guess is
that that would essentially end up making spans optional throughout the
codebase).

Also, some extra care has to be taken to close out the scope in the case
that we do pass in an env, to avoid breaking our assumptions about the
size of the stack when we return from the toplevel

Change-Id: Ie475b2d3dfc72ccbf298d2a3ea28c63ac877d653
Reviewed-on: https://cl.tvl.fyi/c/depot/+/11953
Tested-by: BuildkiteCI
Autosubmit: aspen <root@gws.fyi>
Reviewed-by: flokli <flokli@flokli.de>
2024-07-05 16:39:34 +00:00

121 lines
3.8 KiB
Rust

//! 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 super::GlobalsMap;
use genawaiter::rc::Gen;
use std::rc::Weak;
use crate::{
builtins::coerce_value_to_path,
generators::pin_generator,
observer::NoOpObserver,
value::{Builtin, Thunk},
vm::generators::{self, GenCo},
ErrorKind, SourceCode, Value,
};
async fn import_impl(
co: GenCo,
globals: Weak<GlobalsMap>,
source: SourceCode,
mut args: Vec<Value>,
) -> Result<Value, ErrorKind> {
// TODO(sterni): canon_path()?
let mut path = match coerce_value_to_path(&co, args.pop().unwrap()).await? {
Err(cek) => return Ok(Value::Catchable(Box::new(cek))),
Ok(path) => path,
};
if path.is_dir() {
path.push("default.nix");
}
if let Some(cached) = generators::request_import_cache_lookup(&co, path.clone()).await {
return Ok(cached);
}
let mut reader = generators::request_open_file(&co, path.clone()).await;
// We read to a String instead of a Vec<u8> because rnix only supports
// string source files.
let mut contents = String::new();
reader.read_to_string(&mut contents)?;
let parsed = rnix::ast::Root::parse(&contents);
let errors = parsed.errors();
let file = source.add_file(path.to_string_lossy().to_string(), contents.to_owned());
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()),
// 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"),
None,
&source,
&file,
&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,
});
}
for warning in result.warnings {
generators::emit_warning(&co, warning).await;
}
// Compilation succeeded, we can construct a thunk from whatever it spat
// out and return that.
let res = Value::Thunk(Thunk::new_suspended(
result.lambda,
generators::request_span(&co).await,
));
generators::request_import_cache_put(&co, path, res.clone()).await;
Ok(res)
}
/// Constructs 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",
Some("Import the given file and return the Nix value it evaluates to"),
1,
move |args| {
Gen::new(|co| pin_generator(import_impl(co, globals.clone(), source.clone(), args)))
},
)
}