use std::path::PathBuf; use fix_bytecode::Continuation; use fix_error::Error; use fix_lang::StringId; use fix_runtime::{ AttrSet, Break, BytecodeReader, CallFrame, Machine, MachineExt, Path, PendingLoad, PendingScope, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, canon_path_str, }; use gc_arena::{Gc, Mutation}; use hashbrown::HashSet; pub fn import<'gc, M: Machine<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { // stack: [path] let path_val = m.force_and_retry::(reader, mc)?; let path_str = match ctx.get_string_or_path(path_val) { Some(s) => s.to_owned(), None => { return m.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 m.finish_err(e), }; if let Some(cached) = m.import_cache_get(&abs) { return m.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()); m.push(Value::new(path_sid)); let env = m.env(); m.push_call_frame(CallFrame { pc: Continuation::PImportFinalize.ip() as usize, thunk: None, env, }); m.set_pending_load(PendingLoad { path: abs, scope: None, }); Step::Break(Break::LoadFile) } pub fn import_finalize<'gc, M: Machine<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, ) -> Step { // stack: [path_sid, return_value] let val = m.pop(); #[allow(clippy::unwrap_used)] let path_sid = m.pop().downcast::().unwrap(); // The cache key is keyed by the absolute path string we interned in // `import`. Resolve it back to the host PathBuf. let path_str = ctx.resolve_string(path_sid).to_owned(); m.import_cache_insert(PathBuf::from(path_str), val); m.push(val); let Some(CallFrame { pc: ret_pc, thunk: _, env, }) = m.pop_call_frame() else { unreachable!() }; reader.set_pc(ret_pc); // FIXME: // m.dec_call_depth(); m.set_env(env); Step::Continue(()) } pub fn scoped_import<'gc, M: Machine<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { // stack: [scope, path] let (scope_attrs, path_val) = m.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 m.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 m.finish_err(e), }; let keys: HashSet = scope_attrs.entries.iter().map(|&(k, _)| k).collect(); let slot_id = m.scope_slots_push(Value::new(scope_attrs)); let env = m.env(); m.push_call_frame(CallFrame { pc: Continuation::PScopedImportFinalize.ip() as usize, thunk: None, env, }); m.set_pending_load(PendingLoad { path: abs, scope: Some(PendingScope { keys, slot_id }), }); Step::Break(Break::LoadFile) } pub fn scoped_import_finalize<'gc, M: Machine<'gc>>( m: &mut M, _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 = m.pop(); m.return_from_primop(val, reader) } pub fn path_exists<'gc, M: Machine<'gc>>( m: &mut M, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { let path_val = m.force_and_retry::(reader, mc)?; // 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.downcast::() { (ctx.resolve_string(p.0).to_owned(), true) } else if let Some(s) = ctx.get_string(path_val) { (s.to_owned(), false) } else { return m.finish_err(Error::eval_error(format!( "expected a path or string, got {}", path_val.ty() ))); }; if !is_path_value && !path.starts_with('/') { return m.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() }; m.return_from_primop(Value::new(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) }