refactor: abstract VM
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "fix-primops"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
gc-arena = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
string-interner = { workspace = true }
|
||||
|
||||
fix-abstract-vm = { path = "../fix-abstract-vm" }
|
||||
fix-builtins = { path = "../fix-builtins" }
|
||||
fix-codegen = { path = "../fix-codegen" }
|
||||
fix-common = { path = "../fix-common" }
|
||||
fix-error = { path = "../fix-error" }
|
||||
@@ -0,0 +1,362 @@
|
||||
use fix_abstract_vm::{
|
||||
AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Step, StrictValue, Value,
|
||||
VmRuntimeCtx, VmRuntimeCtxExt,
|
||||
};
|
||||
use fix_builtins::PrimOpPhase;
|
||||
use fix_error::Error;
|
||||
use gc_arena::{Gc, Mutation, RefLock};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub fn seq<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
// stack: [e1, e2] - force e1, return e2
|
||||
m.force_slot(1, reader, mc)?;
|
||||
let e2 = m.pop();
|
||||
let _ = m.pop();
|
||||
m.return_from_primop(e2, reader)
|
||||
}
|
||||
|
||||
pub fn abort<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
ctx: &mut impl VmRuntimeCtx,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
// stack: [msg] - force msg, then abort with it
|
||||
m.force_slot(0, reader, mc)?;
|
||||
let msg_val = m.peek_forced(0);
|
||||
let msg = ctx.get_string(msg_val).unwrap_or("<non-string-value>");
|
||||
m.finish_err(Error::eval_error(format!(
|
||||
"evaluation aborted with the following error message: '{msg}'"
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn deep_seq_force_top<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
// stack: [e1, e2] - force e1, return e2
|
||||
m.force_slot(1, reader, mc)?;
|
||||
|
||||
let e1 = m.peek_forced(1);
|
||||
|
||||
let children: SmallVec<_> = if let Some(attrs) = e1.as_gc::<AttrSet>() {
|
||||
let attrs = &attrs.entries;
|
||||
if attrs.is_empty() {
|
||||
SmallVec::new()
|
||||
} else {
|
||||
attrs.iter().map(|&(_, v)| v).collect()
|
||||
}
|
||||
} else if let Some(list) = e1.as_gc::<List<'gc>>() {
|
||||
let inner = list.inner.borrow();
|
||||
if inner.is_empty() {
|
||||
SmallVec::new()
|
||||
} else {
|
||||
inner.iter().copied().collect()
|
||||
}
|
||||
} else {
|
||||
SmallVec::new()
|
||||
};
|
||||
|
||||
if children.is_empty() {
|
||||
let e2 = m.pop();
|
||||
let _ = m.pop();
|
||||
return m.return_from_primop(e2, reader);
|
||||
}
|
||||
|
||||
let count = children.len() as i32;
|
||||
let seen: Gc<'gc, List<'gc>> = Gc::new(mc, List::default());
|
||||
let worklist: Gc<'gc, List<'gc>> = List::new(mc, children);
|
||||
|
||||
let e2 = m.pop();
|
||||
let _ = m.pop();
|
||||
m.push(e2);
|
||||
m.push(Value::new_gc(seen));
|
||||
m.push(Value::new_gc(worklist));
|
||||
m.push(Value::new_inline(count));
|
||||
reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize);
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
pub fn deep_seq_push<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
// stack: [e2, seen, worklist, counter]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let counter = m.peek(0).as_inline::<i32>().unwrap();
|
||||
if counter == 0 {
|
||||
let _ = m.pop(); // counter
|
||||
let _ = m.pop(); // worklist
|
||||
let _ = m.pop(); // seen
|
||||
let val = m.pop();
|
||||
return m.return_from_primop(val, reader);
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let item = worklist.unlock(mc).borrow_mut().pop().unwrap();
|
||||
m.replace(0, Value::new_inline(counter - 1));
|
||||
m.push(item);
|
||||
|
||||
// force item at TOS, resume at DeepSeqLoop after force
|
||||
m.force_slot_to_pc(0, reader, mc, PrimOpPhase::DeepSeqLoop.ip() as usize)?;
|
||||
reader.set_pc(PrimOpPhase::DeepSeqLoop.ip() as usize);
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
pub fn deep_seq_loop<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
// stack after pop: [e2, seen, worklist, counter]
|
||||
let item = m.pop();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let counter = m.peek(0).as_inline::<i32>().unwrap();
|
||||
|
||||
let mut added: usize = 0;
|
||||
if let Some(attrs) = item.as_gc::<AttrSet>() {
|
||||
let attrs = &attrs.entries;
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let seen = m.peek_forced(2).as_gc::<List<'gc>>().unwrap();
|
||||
if !is_value_in_seen(seen, item) {
|
||||
add_value_to_seen(seen, mc, item);
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
|
||||
{
|
||||
let mut wl = worklist.unlock(mc).borrow_mut();
|
||||
for &(_, v) in attrs.iter() {
|
||||
wl.push(v);
|
||||
}
|
||||
added = attrs.len();
|
||||
}
|
||||
}
|
||||
} else if let Some(list) = item.as_gc::<List<'gc>>() {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let seen = m.peek_forced(2).as_gc::<List<'gc>>().unwrap();
|
||||
if !is_value_in_seen(seen, item) {
|
||||
add_value_to_seen(seen, mc, item);
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let worklist = m.peek_forced(1).as_gc::<List<'gc>>().unwrap();
|
||||
{
|
||||
let inner = list.inner.borrow();
|
||||
let mut wl = worklist.unlock(mc).borrow_mut();
|
||||
for &v in inner.iter() {
|
||||
wl.push(v);
|
||||
}
|
||||
added = inner.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.replace(0, Value::new_inline(counter + added as i32));
|
||||
reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize);
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
pub fn force_result_shallow<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
ctx: &mut impl VmRuntimeCtx,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
m.force_slot(0, reader, mc)?;
|
||||
let val = m.peek_forced(0);
|
||||
|
||||
let (count, has_children) = if let Some(attrs) = val.as_gc::<AttrSet>() {
|
||||
let len = attrs.entries.len();
|
||||
(len, len > 0)
|
||||
} else if let Some(list) = val.as_gc::<List<'gc>>() {
|
||||
let len = list.inner.borrow().len();
|
||||
(len, len > 0)
|
||||
} else {
|
||||
(0, false)
|
||||
};
|
||||
|
||||
if !has_children {
|
||||
let val = m.pop();
|
||||
return m.finish_ok(ctx.convert_value(val));
|
||||
}
|
||||
|
||||
m.push(Value::new_inline(0i32));
|
||||
m.push(Value::new_inline(count as i32));
|
||||
reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize);
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
pub fn force_result_shallow_push<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
ctx: &mut impl VmRuntimeCtx,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let idx = m.peek(1).as_inline::<i32>().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let len = m.peek(0).as_inline::<i32>().unwrap();
|
||||
|
||||
if idx == len {
|
||||
let _ = m.pop(); // len
|
||||
let _ = m.pop(); // idx
|
||||
let val = m.pop();
|
||||
return m.finish_ok(ctx.convert_value(val));
|
||||
}
|
||||
|
||||
let val = m.peek_forced(2);
|
||||
let child = if let Some(attrs) = val.as_gc::<AttrSet>() {
|
||||
attrs.entries.get(idx as usize).map(|&(_, v)| v)
|
||||
} else if let Some(list) = val.as_gc::<List<'gc>>() {
|
||||
list.inner.borrow().get(idx as usize).copied()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(child) = child {
|
||||
m.replace(1, Value::new_inline(idx + 1));
|
||||
m.push(child);
|
||||
m.force_slot_to_pc(
|
||||
0,
|
||||
reader,
|
||||
mc,
|
||||
PrimOpPhase::ForceResultShallowLoop.ip() as usize,
|
||||
)?;
|
||||
reader.set_pc(PrimOpPhase::ForceResultShallowLoop.ip() as usize);
|
||||
}
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
pub fn force_result_shallow_loop<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
_mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
let _ = m.pop(); // forced child
|
||||
reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize);
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
pub fn force_result_deep_finish<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
ctx: &mut impl VmRuntimeCtx,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
|
||||
m.finish_ok(ctx.convert_value(val.relax()))
|
||||
}
|
||||
|
||||
fn is_value_in_seen<'gc>(seen: Gc<'gc, List<'gc>>, val: Value<'gc>) -> bool {
|
||||
if !is_container(val) {
|
||||
return false;
|
||||
}
|
||||
let target = val.to_bits();
|
||||
for &v in seen.inner.borrow().iter() {
|
||||
if v.to_bits() == target {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn add_value_to_seen<'gc>(seen: Gc<'gc, List<'gc>>, mc: &Mutation<'gc>, val: Value<'gc>) {
|
||||
if is_container(val) {
|
||||
seen.unlock(mc).borrow_mut().push(val);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_functor_1<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
// Stack invariant on every (re-)entry: [..., orig_arg, self, functor]
|
||||
// where `functor` is TOS. Retries during force land back here safely.
|
||||
let functor = m.force_and_retry::<StrictValue>(reader, mc)?;
|
||||
// Stack now: [..., orig_arg, self]
|
||||
let self_val = m.pop();
|
||||
m.push(functor.relax());
|
||||
// Stack: [..., orig_arg, functor]
|
||||
// Call 1: functor(self). Resume into CallFunctor2 once it returns.
|
||||
m.call(
|
||||
reader,
|
||||
mc,
|
||||
self_val,
|
||||
PrimOpPhase::CallFunctor2.ip() as usize,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn call_functor_2<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
// Stack on entry: [..., orig_arg, intermediate]
|
||||
// call_stack top: synthetic frame with caller's resume_pc.
|
||||
let intermediate = m.pop();
|
||||
let orig_arg = m.pop();
|
||||
let saved = m.pop_call_frame().expect("functor outer frame missing");
|
||||
m.set_env(saved.env);
|
||||
m.push(intermediate);
|
||||
// Call 2: intermediate(orig_arg). Resume to caller.
|
||||
m.call(reader, mc, orig_arg, saved.pc)
|
||||
}
|
||||
|
||||
pub fn call_pattern<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
ctx: &mut impl VmRuntimeCtx,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
let (func, attrset) = m.force_and_retry::<(Gc<Closure>, Gc<AttrSet>)>(reader, mc)?;
|
||||
|
||||
let Closure {
|
||||
ip,
|
||||
n_locals,
|
||||
env,
|
||||
pattern,
|
||||
} = *func;
|
||||
let Some(pattern) = pattern else {
|
||||
unreachable!()
|
||||
};
|
||||
// TODO: get function name
|
||||
// TODO: param spans
|
||||
if !pattern.ellipsis {
|
||||
for key in pattern.required.iter().copied() {
|
||||
if attrset.lookup(key).is_none() {
|
||||
let name = ctx.resolve_string(key);
|
||||
return m.finish_err(Error::eval_error(format!(
|
||||
"function 'anonymous lambda' called without required argument '{name}'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
for &(key, _) in attrset.entries.iter() {
|
||||
let is_expected = pattern.required.contains(&key) || pattern.optional.contains(&key);
|
||||
if !is_expected {
|
||||
let name = ctx.resolve_string(key);
|
||||
return m.finish_err(Error::eval_error(format!(
|
||||
"function 'anonymous lambda' called with unexpected argument '{name}'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let new_env = Gc::new(
|
||||
mc,
|
||||
RefLock::new(Env::with_arg(Value::new_gc(attrset), n_locals, env)),
|
||||
);
|
||||
reader.set_pc(ip as usize);
|
||||
m.set_env(new_env);
|
||||
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
fn is_container(val: Value<'_>) -> bool {
|
||||
val.is::<AttrSet>() || val.is::<List<'_>>()
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use fix_abstract_vm::{
|
||||
BytecodeReader, Machine, MachineExt, NixString, NixType, Path, Step, StrictValue, Value,
|
||||
VmRuntimeCtx,
|
||||
};
|
||||
use fix_common::StringId;
|
||||
use fix_error::Error;
|
||||
use gc_arena::Mutation;
|
||||
|
||||
pub fn to_string<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
_ctx: &mut impl VmRuntimeCtx,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
|
||||
if val.is::<StringId>() || val.is::<NixString>() {
|
||||
return m.return_from_primop(val.relax(), reader);
|
||||
}
|
||||
if let Some(p) = val.as_inline::<Path>() {
|
||||
return m.return_from_primop(Value::new_inline(p.0), reader);
|
||||
}
|
||||
// TODO: derivations / `__toString` / `outPath`,
|
||||
// numbers, lists.
|
||||
m.finish_err(Error::eval_error(format!(
|
||||
"cannot coerce {} to a string",
|
||||
val.ty()
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn type_of<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
ctx: &mut impl VmRuntimeCtx,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
|
||||
let name: &str = match val.ty() {
|
||||
NixType::Int => "int",
|
||||
NixType::Float => "float",
|
||||
NixType::Bool => "bool",
|
||||
NixType::Null => "null",
|
||||
NixType::String => "string",
|
||||
NixType::Path => "path",
|
||||
NixType::AttrSet => "set",
|
||||
NixType::List => "list",
|
||||
NixType::Closure | NixType::PrimOp | NixType::PrimOpApp => "lambda",
|
||||
NixType::Thunk => unreachable!("forced"),
|
||||
};
|
||||
let sid = ctx.intern_string(name);
|
||||
m.return_from_primop(Value::new_inline(sid), reader)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use fix_abstract_vm::{
|
||||
AttrSet, Break, BytecodeReader, CallFrame, Machine, MachineExt, Path, PendingLoad,
|
||||
PendingScope, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, canon_path_str,
|
||||
};
|
||||
use fix_builtins::PrimOpPhase;
|
||||
use fix_common::StringId;
|
||||
use fix_error::Error;
|
||||
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::<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),
|
||||
};
|
||||
|
||||
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_inline(path_sid));
|
||||
let env = m.env();
|
||||
m.push_call_frame(CallFrame {
|
||||
pc: PrimOpPhase::ImportFinalize.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().as_inline::<StringId>().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<AttrSet>, 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<StringId> = scope_attrs.entries.iter().map(|&(k, _)| k).collect();
|
||||
let slot_id = m.scope_slots_push(Value::new_gc(scope_attrs));
|
||||
|
||||
let env = m.env();
|
||||
m.push_call_frame(CallFrame {
|
||||
pc: PrimOpPhase::ScopedImportFinalize.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::<StrictValue>(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.as_inline::<Path>() {
|
||||
(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_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)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
mod control;
|
||||
mod conv;
|
||||
mod io;
|
||||
mod list;
|
||||
mod path;
|
||||
|
||||
pub use control::*;
|
||||
pub use conv::*;
|
||||
use fix_abstract_vm::{BytecodeReader, Machine, Step, VmRuntimeCtx};
|
||||
use fix_builtins::PrimOpPhase;
|
||||
use fix_error::Error;
|
||||
use gc_arena::Mutation;
|
||||
pub use io::*;
|
||||
pub use list::*;
|
||||
pub use path::*;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn dispatch_primop<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
ctx: &mut impl VmRuntimeCtx,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
use PrimOpPhase::*;
|
||||
let phase_disc = reader.read_u8();
|
||||
let Ok(phase) = PrimOpPhase::try_from(phase_disc) else {
|
||||
return m.finish_err(Error::eval_error("invalid primop phase"));
|
||||
};
|
||||
match phase {
|
||||
Abort => abort(m, ctx, reader, mc),
|
||||
|
||||
DeepSeq => deep_seq_force_top(m, reader, mc),
|
||||
DeepSeqPush => deep_seq_push(m, reader, mc),
|
||||
DeepSeqLoop => deep_seq_loop(m, reader, mc),
|
||||
Seq => seq(m, reader, mc),
|
||||
|
||||
FilterForceList => filter_force_list(m, reader, mc),
|
||||
FilterCallPred => filter_call_pred(m, reader, mc),
|
||||
FilterCheck => filter_check(m, reader, mc),
|
||||
|
||||
FoldlStrict => foldl_strict_entry(m, reader, mc),
|
||||
FoldlStrictEmpty => foldl_strict_empty(m, reader, mc),
|
||||
FoldlStrictCall1 => foldl_strict_call1(m, reader, mc),
|
||||
FoldlStrictCall2 => foldl_strict_call2(m, reader, mc),
|
||||
FoldlStrictUpdate => foldl_strict_update(m, reader, mc),
|
||||
|
||||
ForceResultShallow => force_result_shallow(m, ctx, reader, mc),
|
||||
ForceResultShallowPush => force_result_shallow_push(m, ctx, reader, mc),
|
||||
ForceResultShallowLoop => force_result_shallow_loop(m, reader, mc),
|
||||
ForceResultDeepFinish => force_result_deep_finish(m, ctx, reader, mc),
|
||||
|
||||
CallPattern => call_pattern(m, ctx, reader, mc),
|
||||
CallFunctor1 => call_functor_1(m, reader, mc),
|
||||
CallFunctor2 => call_functor_2(m, reader, mc),
|
||||
|
||||
Import => import(m, ctx, reader, mc),
|
||||
ImportFinalize => import_finalize(m, ctx, reader),
|
||||
ScopedImport => scoped_import(m, ctx, reader, mc),
|
||||
ScopedImportFinalize => scoped_import_finalize(m, ctx, reader, mc),
|
||||
|
||||
PathExists => path_exists(m, ctx, reader, mc),
|
||||
ToPath => to_path(m, ctx, reader, mc),
|
||||
IsPath => is_path(m, reader, mc),
|
||||
ToString => to_string(m, ctx, reader, mc),
|
||||
TypeOf => type_of(m, ctx, reader, mc),
|
||||
|
||||
phase => todo!("primop phase {phase:?}"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use fix_abstract_vm::{
|
||||
BytecodeReader, List, Machine, MachineExt, NixType, Step, StrictValue, Value,
|
||||
};
|
||||
use fix_builtins::PrimOpPhase;
|
||||
use gc_arena::Mutation;
|
||||
|
||||
pub fn filter_force_list<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
m.force_slot(0, reader, mc)?;
|
||||
let list = match m.peek_forced(0).expect_gc::<List>() {
|
||||
Ok(list) => list,
|
||||
Err(got) => return m.finish_type_err(NixType::List, got),
|
||||
};
|
||||
if list.inner.borrow().is_empty() {
|
||||
let val = m.pop();
|
||||
return m.return_from_primop(val, reader);
|
||||
}
|
||||
// prepare stack layout: [ pred list idx acc ]
|
||||
m.push(Value::new_inline(0));
|
||||
m.push(Value::new_gc(List::new_gc(mc)));
|
||||
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
pub fn filter_call_pred<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
m.force_slot(3, reader, mc)?;
|
||||
let pred = m.peek_forced(3);
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let idx = m.peek(1).as_inline::<i32>().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let elem = m.peek_forced(2).as_gc::<List>().unwrap().inner.borrow()[idx as usize];
|
||||
m.push(pred.relax());
|
||||
m.call(reader, mc, elem, PrimOpPhase::FilterCheck.ip() as usize)
|
||||
}
|
||||
|
||||
pub fn filter_check<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
let ret = m.force_and_retry::<bool>(reader, mc)?;
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let idx = m.peek(1).as_inline::<i32>().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let list = m.peek_forced(2).as_gc::<List>().unwrap();
|
||||
let list = list.inner.borrow();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let acc = m.peek_forced(0).as_gc::<List>().unwrap();
|
||||
if ret {
|
||||
let mut acc = acc.unlock(mc).borrow_mut();
|
||||
acc.push(list[idx as usize]);
|
||||
}
|
||||
if idx as usize == list.len() - 1 {
|
||||
let acc = m.pop();
|
||||
let _ = m.pop(); // idx
|
||||
let _ = m.pop(); // list
|
||||
let _ = m.pop(); // pred
|
||||
return m.return_from_primop(acc, reader);
|
||||
}
|
||||
m.replace(1, Value::new_inline(idx + 1));
|
||||
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
// foldl' op nul list
|
||||
//
|
||||
// Stack layouts across phases:
|
||||
// Entry: [op, nul, list]
|
||||
// Empty: [op, nul]
|
||||
// Call1: [op, list, idx, acc]
|
||||
// Call2: [op, list, idx, acc, intermediate]
|
||||
// Update: [op, list, idx, acc, result]
|
||||
pub fn foldl_strict_entry<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
m.force_slot(0, reader, mc)?;
|
||||
let list_val = m.peek_forced(0);
|
||||
let Some(list) = list_val.as_gc::<List>() else {
|
||||
return m.finish_type_err(NixType::List, list_val.ty());
|
||||
};
|
||||
if list.inner.borrow().is_empty() {
|
||||
let _ = m.pop(); // list
|
||||
reader.set_pc(PrimOpPhase::FoldlStrictEmpty.ip() as usize);
|
||||
return Step::Continue(());
|
||||
}
|
||||
let list_val = m.pop();
|
||||
let nul_val = m.pop();
|
||||
m.push(list_val);
|
||||
m.push(Value::new_inline(0i32));
|
||||
m.push(nul_val);
|
||||
reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize);
|
||||
Step::Continue(())
|
||||
}
|
||||
|
||||
pub fn foldl_strict_empty<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
let nul = m.force_and_retry::<StrictValue>(reader, mc)?;
|
||||
let _ = m.pop(); // op
|
||||
m.return_from_primop(nul.relax(), reader)
|
||||
}
|
||||
|
||||
pub fn foldl_strict_call1<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
m.force_slot(3, reader, mc)?;
|
||||
let op = m.peek_forced(3);
|
||||
let acc = m.peek(0);
|
||||
m.push(op.relax());
|
||||
m.call(reader, mc, acc, PrimOpPhase::FoldlStrictCall2.ip() as usize)
|
||||
}
|
||||
|
||||
pub fn foldl_strict_call2<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let idx = m.peek(2).as_inline::<i32>().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let list = m.peek_forced(3).as_gc::<List>().unwrap();
|
||||
let elem = list.inner.borrow()[idx as usize];
|
||||
m.call(
|
||||
reader,
|
||||
mc,
|
||||
elem,
|
||||
PrimOpPhase::FoldlStrictUpdate.ip() as usize,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn foldl_strict_update<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
_mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
let result = m.pop();
|
||||
m.replace(0, result);
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let idx = m.peek(1).as_inline::<i32>().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let list = m.peek_forced(2).as_gc::<List>().unwrap();
|
||||
let len = list.inner.borrow().len();
|
||||
if (idx as usize) + 1 == len {
|
||||
let acc = m.pop();
|
||||
let _ = m.pop(); // idx
|
||||
let _ = m.pop(); // list
|
||||
let _ = m.pop(); // op
|
||||
return m.return_from_primop(acc, reader);
|
||||
}
|
||||
m.replace(1, Value::new_inline(idx + 1));
|
||||
reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize);
|
||||
Step::Continue(())
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use fix_abstract_vm::{
|
||||
BytecodeReader, Machine, MachineExt, Path, Step, StrictValue, Value, VmRuntimeCtx,
|
||||
VmRuntimeCtxExt, canon_path_str,
|
||||
};
|
||||
use fix_error::Error;
|
||||
use gc_arena::Mutation;
|
||||
|
||||
pub fn to_path<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
ctx: &mut impl VmRuntimeCtx,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
// coerce to path THEN TO STRING
|
||||
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
|
||||
if let Some(Path(s)) = val.as_inline::<Path>() {
|
||||
return m.return_from_primop(Value::new_inline(s), reader);
|
||||
}
|
||||
let Some(s) = ctx.get_string(val) else {
|
||||
return m.finish_err(Error::eval_error(format!(
|
||||
"cannot coerce {} to a path",
|
||||
val.ty()
|
||||
)));
|
||||
};
|
||||
if !s.starts_with('/') {
|
||||
return m.finish_err(Error::eval_error(format!(
|
||||
"string '{s}' doesn't represent an absolute path"
|
||||
)));
|
||||
}
|
||||
let canon = canon_path_str(s);
|
||||
let sid = ctx.intern_string(canon);
|
||||
m.return_from_primop(Value::new_inline(sid), reader)
|
||||
}
|
||||
|
||||
pub fn is_path<'gc, M: Machine<'gc>>(
|
||||
m: &mut M,
|
||||
reader: &mut BytecodeReader<'_>,
|
||||
mc: &Mutation<'gc>,
|
||||
) -> Step {
|
||||
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
|
||||
let is_path = val.is::<Path>();
|
||||
m.return_from_primop(Value::new_inline(is_path), reader)
|
||||
}
|
||||
Reference in New Issue
Block a user