use builtin_macros::builtins; use smol_str::SmolStr; use std::{ collections::BTreeMap, env, rc::{Rc, Weak}, time::{SystemTime, UNIX_EPOCH}, }; use crate::{ compiler::GlobalsMap, errors::ErrorKind, io::FileType, observer::NoOpObserver, value::{Builtin, BuiltinArgument, NixAttrs, Thunk}, vm::VM, SourceCode, Value, }; #[builtins] mod impure_builtins { use super::*; use crate::builtins::coerce_value_to_path; #[builtin("getEnv")] fn builtin_get_env(_: &mut VM, var: Value) -> Result<Value, ErrorKind> { Ok(env::var(var.to_str()?).unwrap_or_else(|_| "".into()).into()) } #[builtin("pathExists")] fn builtin_path_exists(vm: &mut VM, s: Value) -> Result<Value, ErrorKind> { let path = coerce_value_to_path(&s, vm)?; vm.io().path_exists(path).map(Value::Bool) } #[builtin("readDir")] fn builtin_read_dir(vm: &mut VM, path: Value) -> Result<Value, ErrorKind> { let path = coerce_value_to_path(&path, vm)?; let res = vm.io().read_dir(path)?.into_iter().map(|(name, ftype)| { ( name, Value::String( SmolStr::new(match ftype { FileType::Directory => "directory", FileType::Regular => "regular", FileType::Symlink => "symlink", FileType::Unknown => "unknown", }) .into(), ), ) }); Ok(Value::attrs(NixAttrs::from_iter(res))) } #[builtin("readFile")] fn builtin_read_file(vm: &mut VM, path: Value) -> Result<Value, ErrorKind> { let path = coerce_value_to_path(&path, vm)?; vm.io() .read_to_string(path) .map(|s| Value::String(s.into())) } } /// Return all impure builtins, that is all builtins which may perform I/O /// outside of the VM and so cannot be used in all contexts (e.g. WASM). pub(super) fn builtins() -> BTreeMap<&'static str, Value> { let mut map: BTreeMap<&'static str, Value> = impure_builtins::builtins() .into_iter() .map(|b| (b.name(), Value::Builtin(b))) .collect(); map.insert( "storeDir", Value::Thunk(Thunk::new_suspended_native(Rc::new(Box::new( |vm: &mut VM| match vm.io().store_dir() { None => Ok(Value::Null), Some(dir) => Ok(Value::String(dir.into())), }, )))), ); // currentTime pins the time at which evaluation was started { let seconds = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(dur) => dur.as_secs() as i64, // This case is hit if the system time is *before* epoch. Err(err) => -(err.duration().as_secs() as i64), }; map.insert("currentTime", Value::Integer(seconds)); } map } /// 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. pub 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 = super::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) }, ) }