use std::path::PathBuf; use fix_builtins::PrimOpPhase; use fix_common::StringId; use fix_error::Error; use gc_arena::{Gc, Mutation}; use hashbrown::HashSet; use crate::bytecode_reader::BytecodeReader; use crate::instructions::misc::canon_path_str; use crate::value::*; use crate::{Break, CallFrame, PendingLoad, PendingScope, Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt}; impl<'gc> Vm<'gc> { pub(crate) fn primop_import( &mut self, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { // stack: [path] let path_val = self.force_and_retry::(reader, mc)?; let path_str = match ctx.get_string_or_path(path_val) { Some(s) => s.to_owned(), None => { return self.finish_err(Error::eval_error(format!( "expected a path or string, got {}", path_val.ty() ))); } }; let abs = match resolve_import_target(&path_str) { Ok(p) => p, Err(e) => return self.finish_err(e), }; if let Some(&cached) = self.import_cache.get(&abs) { return self.return_from_primop(cached, reader); } // Stash the resolved path on the stack as a string-id so the // finalizer can use it as the cache key. The slot we pop here was // freed by `force_and_retry`, so we simply push. let path_sid = ctx.intern_string(abs.to_string_lossy()); self.push(Value::new_inline(path_sid)); self.call_stack.push(CallFrame { pc: PrimOpPhase::ImportFinalize.ip() as usize, thunk: None, env: self.env, }); self.pending_load = Some(PendingLoad { path: abs, scope: None, }); Step::Break(Break::LoadFile) } pub(crate) fn primop_import_finalize( &mut self, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, ) -> Step { // stack: [path_sid, return_value] let val = self.pop(); #[allow(clippy::unwrap_used)] let path_sid = self.pop().as_inline::().unwrap(); // The cache key is keyed by the absolute path string we interned in // `primop_import`. Resolve it back to the host PathBuf. let path_str = ctx.resolve_string(path_sid).to_owned(); self.import_cache.insert(PathBuf::from(path_str), val); self.push(val); let Some(CallFrame { pc: ret_pc, thunk: _, env, }) = self.call_stack.pop() else { unreachable!() }; reader.set_pc(ret_pc); // FIXME: // self.call_depth -= 1; self.env = env; Step::Continue(()) } pub(crate) fn primop_scoped_import( &mut self, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { // stack: [scope, path] let (scope_attrs, path_val) = self.force_and_retry::<(Gc, StrictValue)>(reader, mc)?; let path_str = match ctx.get_string_or_path(path_val) { Some(s) => s.to_owned(), None => { return self.finish_err(Error::eval_error(format!( "expected a path or string, got {}", path_val.ty() ))); } }; let abs = match resolve_import_target(&path_str) { Ok(p) => p, Err(e) => return self.finish_err(e), }; let keys: HashSet = scope_attrs.entries.iter().map(|&(k, _)| k).collect(); let slot_id = self.scope_slots.len() as u32; self.scope_slots.push(Value::new_gc(scope_attrs)); self.call_stack.push(CallFrame { pc: PrimOpPhase::ScopedImportFinalize.ip() as usize, thunk: None, env: self.env, }); self.pending_load = Some(PendingLoad { path: abs, scope: Some(PendingScope { keys, slot_id }), }); Step::Break(Break::LoadFile) } pub(crate) fn primop_scoped_import_finalize( &mut self, _ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, _mc: &Mutation<'gc>, ) -> Step { // stack: [return_value] // We intentionally do NOT pop the slot from `scope_slots` so that // closures or thunks created inside the imported file can still // resolve their scope after `scopedImport` returns. let val = self.pop(); self.return_from_primop(val, reader) } pub(crate) fn primop_path_exists( &mut self, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { let path_val = self.force_and_retry::(reader, mc)?; // CppNix: pathExists requires an absolute path. A `Path` value is // always absolute; a string is accepted only if it starts with `/`. let (path, is_path_value) = if let Some(p) = path_val.as_inline::() { (ctx.resolve_string(p.0).to_owned(), true) } else if let Some(s) = ctx.get_string(path_val) { (s.to_owned(), false) } else { return self.finish_err(Error::eval_error(format!( "expected a path or string, got {}", path_val.ty() ))); }; if !is_path_value && !path.starts_with('/') { return self.finish_err(Error::eval_error(format!( "string '{path}' doesn't represent an absolute path" ))); } // CppNix collapses consecutive slashes and resolves `.` / `..` lexically // before checking. Trailing-slash / trailing-dot mean "must be a directory". let must_be_dir = path.ends_with('/') || path.ends_with("/."); let canon = canon_path_str(&path); let p = std::path::Path::new(&canon); let exists = if must_be_dir { std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false) } else { std::fs::symlink_metadata(p).is_ok() }; self.return_from_primop(Value::new_inline(exists), reader) } } /// Convert the user-supplied path string into an absolute, dotted-segment /// resolved `PathBuf` and append `default.nix` if the target is a directory. fn resolve_import_target(path: &str) -> Result> { let mut abs = PathBuf::from(path); if !abs.is_absolute() { return Err(Error::eval_error(format!( "import: expected an absolute path, got '{path}'" ))); } if abs.is_dir() { abs.push("default.nix"); } Ok(abs) }