refactor: abstract VM

This commit is contained in:
2026-05-13 18:28:18 +08:00
parent 21899f7380
commit 29fab93cd1
42 changed files with 1823 additions and 1410 deletions
+17
View File
@@ -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" }
+362
View File
@@ -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<'_>>()
}
+51
View File
@@ -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)
}
+190
View File
@@ -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)
}
+69
View File
@@ -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:?}"),
}
}
+166
View File
@@ -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(())
}
+43
View File
@@ -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)
}