182 lines
5.9 KiB
Rust
182 lines
5.9 KiB
Rust
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::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::<StrictValue>(reader, mc)?;
|
|
let path_str = match ctx.get_string(path_val) {
|
|
Some(s) => s.to_owned(),
|
|
None => {
|
|
return self.finish_err(Error::eval_error(format!(
|
|
"expected a path 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::<StringId>().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<AttrSet>, StrictValue)>(reader, mc)?;
|
|
let path_str = match ctx.get_string(path_val) {
|
|
Some(s) => s.to_owned(),
|
|
None => {
|
|
return self.finish_err(Error::eval_error(format!(
|
|
"expected a path 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<StringId> = 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::<StrictValue>(reader, mc)?;
|
|
let path = match ctx.get_string(path_val) {
|
|
Some(s) => s.to_owned(),
|
|
None => {
|
|
return self.finish_err(Error::eval_error(format!(
|
|
"expected a path string, got {}",
|
|
path_val.ty()
|
|
)));
|
|
}
|
|
};
|
|
let must_be_dir = path.ends_with('/') || path.ends_with("/.");
|
|
let p = std::path::Path::new(&path);
|
|
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<PathBuf, Box<Error>> {
|
|
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)
|
|
}
|