snix_eval/compiler/
import.rs

1//! This module implements the Nix language's `import` feature, which
2//! is exposed as a builtin in the Nix language.
3//!
4//! This is not a typical builtin, as it needs access to internal
5//! compiler and VM state (such as the [`crate::SourceCode`]
6//! instance, or observers).
7
8use super::GlobalsMap;
9use genawaiter::rc::Gen;
10use std::rc::Weak;
11
12use crate::{
13    ErrorKind, SourceCode, Value,
14    builtins::coerce_value_to_path,
15    generators::pin_generator,
16    observer::NoOpObserver,
17    try_cek_to_value,
18    value::{Builtin, Thunk},
19    vm::generators::{self, GenCo},
20};
21
22async fn import_impl(
23    co: GenCo,
24    globals: Weak<GlobalsMap>,
25    source: SourceCode,
26    mut args: Vec<Value>,
27) -> Result<Value, ErrorKind> {
28    // TODO(sterni): canon_path()?
29    let mut path = try_cek_to_value!(coerce_value_to_path(&co, args.pop().unwrap()).await?);
30
31    if path.is_dir() {
32        path.push("default.nix");
33    }
34
35    if let Some(cached) = generators::request_import_cache_lookup(&co, path.clone()).await {
36        return Ok(cached);
37    }
38
39    let mut reader = generators::request_open_file(&co, path.clone()).await;
40    // We read to a String instead of a Vec<u8> because rnix only supports
41    // string source files.
42    let mut contents = String::new();
43    reader.read_to_string(&mut contents)?;
44
45    let parsed = rnix::ast::Root::parse(&contents);
46    let errors = parsed.errors();
47    let file = source.add_file(path.to_string_lossy().to_string(), contents.to_owned());
48
49    if !errors.is_empty() {
50        return Err(ErrorKind::ImportParseError {
51            path,
52            file,
53            errors: errors.to_vec(),
54        });
55    }
56
57    let result = crate::compiler::compile(
58        &parsed.tree().expr().unwrap(),
59        Some(path.clone()),
60        // The VM must ensure that a strong reference to the globals outlives
61        // any self-references (which are weak) embedded within the globals. If
62        // the expect() below panics, it means that did not happen.
63        globals
64            .upgrade()
65            .expect("globals dropped while still in use"),
66        None,
67        &source,
68        &file,
69        &mut NoOpObserver::default(),
70    )
71    .map_err(|err| ErrorKind::ImportCompilerError {
72        path: path.clone(),
73        errors: vec![err],
74    })?;
75
76    if !result.errors.is_empty() {
77        return Err(ErrorKind::ImportCompilerError {
78            path,
79            errors: result.errors,
80        });
81    }
82
83    for warning in result.warnings {
84        generators::emit_warning(&co, warning).await;
85    }
86
87    // Compilation succeeded, we can construct a thunk from whatever it spat
88    // out and return that.
89    let res = Value::Thunk(Thunk::new_suspended(
90        result.lambda,
91        generators::request_span(&co).await,
92    ));
93
94    generators::request_import_cache_put(&co, path, res.clone()).await;
95
96    Ok(res)
97}
98
99/// Constructs the `import` builtin. This builtin is special in that
100/// it needs to capture the [crate::SourceCode] structure to correctly
101/// track source code locations while invoking a compiler.
102// TODO: need to be able to pass through a CompilationObserver, too.
103// TODO: can the `SourceCode` come from the compiler?
104pub(super) fn builtins_import(globals: &Weak<GlobalsMap>, source: SourceCode) -> Builtin {
105    // This (very cheap, once-per-compiler-startup) clone exists
106    // solely in order to keep the borrow checker happy.  It
107    // resolves the tension between the requirements of
108    // Rc::new_cyclic() and Builtin::new()
109    let globals = globals.clone();
110
111    Builtin::new(
112        "import",
113        Some("Import the given file and return the Nix value it evaluates to"),
114        1,
115        move |args| {
116            Gen::new(|co| pin_generator(import_impl(co, globals.clone(), source.clone(), args)))
117        },
118    )
119}