394 lines
13 KiB
Rust
394 lines
13 KiB
Rust
use fix_builtins::PrimOpPhase;
|
|
use fix_error::Error;
|
|
use gc_arena::{Gc, Mutation};
|
|
use num_enum::TryFromPrimitive as _;
|
|
use smallvec::SmallVec;
|
|
|
|
use crate::value::*;
|
|
use crate::{BytecodeReader, CallFrame, Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt};
|
|
|
|
impl<'gc> Vm<'gc> {
|
|
#[allow(clippy::too_many_lines)]
|
|
pub(crate) fn op_dispatch_primop(
|
|
&mut self,
|
|
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_primitive(phase_disc) else {
|
|
return self.finish_err(Error::eval_error("invalid primop phase"));
|
|
};
|
|
match phase {
|
|
Abort => self.primop_abort(ctx, reader, mc),
|
|
|
|
DeepSeq => self.primop_deep_seq_force_top(reader, mc),
|
|
DeepSeqPush => self.primop_deep_seq_push(reader, mc),
|
|
DeepSeqLoop => self.primop_deep_seq_loop(reader, mc),
|
|
Seq => self.primop_seq(reader, mc),
|
|
|
|
FilterForceList => self.primop_filter_force_list(reader, mc),
|
|
FilterCallPred => self.primop_filter_call_pred(reader, mc),
|
|
FilterCheck => self.primop_filter_check(reader, mc),
|
|
|
|
ForceResultShallow => self.primop_force_result_shallow(ctx, reader, mc),
|
|
ForceResultShallowPush => self.primop_force_result_shallow_push(ctx, reader, mc),
|
|
ForceResultShallowLoop => self.primop_force_result_shallow_loop(reader, mc),
|
|
ForceResultDeepFinish => self.primop_force_result_deep_finish(ctx, reader, mc),
|
|
|
|
phase => todo!("primop phase {phase:?}"),
|
|
}
|
|
}
|
|
|
|
fn return_from_primop(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
let Some(CallFrame {
|
|
pc: ret_pc,
|
|
stack_depth: _,
|
|
thunk: _,
|
|
env,
|
|
with_env,
|
|
}) = self.call_stack.pop()
|
|
else {
|
|
unreachable!()
|
|
};
|
|
reader.set_pc(ret_pc);
|
|
self.call_depth -= 1;
|
|
self.env = env;
|
|
self.with_env = with_env;
|
|
Step::Continue(())
|
|
}
|
|
|
|
pub(crate) fn primop_filter_force_list(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
self.force_slot(0, reader, mc)?;
|
|
let list = match self.peek_forced(0).expect_gc::<List>() {
|
|
Ok(list) => list,
|
|
Err(got) => return self.finish_type_err(NixType::List, got),
|
|
};
|
|
if list.inner.borrow().is_empty() {
|
|
return self.return_from_primop(reader);
|
|
}
|
|
// prepare stack layout: [ pred list idx acc ]
|
|
self.push(Value::new_inline(0));
|
|
self.push(Value::new_gc(List::new_gc(mc)));
|
|
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
|
|
Step::Continue(())
|
|
}
|
|
|
|
pub(crate) fn primop_filter_call_pred(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
self.force_slot(3, reader, mc)?;
|
|
let pred = self.peek_forced(3);
|
|
#[allow(clippy::unwrap_used)]
|
|
let idx = self.peek(1).as_inline::<i32>().unwrap();
|
|
#[allow(clippy::unwrap_used)]
|
|
let elem = self.peek_forced(2).as_gc::<List>().unwrap().inner.borrow()[idx as usize];
|
|
self.push(pred.relax());
|
|
self.call(reader, mc, elem, PrimOpPhase::FilterCheck.ip() as usize)
|
|
}
|
|
|
|
pub(crate) fn primop_filter_check(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
let ret = self.try_force::<bool>(reader, mc)?;
|
|
#[allow(clippy::unwrap_used)]
|
|
let idx = self.peek(1).as_inline::<i32>().unwrap();
|
|
#[allow(clippy::unwrap_used)]
|
|
let list = self.peek_forced(2).as_gc::<List>().unwrap();
|
|
let list = list.inner.borrow();
|
|
#[allow(clippy::unwrap_used)]
|
|
let acc = self.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 = self.pop();
|
|
let _ = self.pop(); // idx
|
|
let _ = self.pop(); // list
|
|
let _ = self.pop(); // pred
|
|
self.push(acc);
|
|
return self.return_from_primop(reader);
|
|
}
|
|
self.replace(1, Value::new_inline(idx + 1));
|
|
reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize);
|
|
Step::Continue(())
|
|
}
|
|
|
|
pub(crate) fn primop_seq(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
// stack: [e1, e2] - force e1, return e2
|
|
self.force_slot(1, reader, mc)?;
|
|
let e2 = self.pop();
|
|
let _ = self.pop();
|
|
self.push(e2);
|
|
self.return_from_primop(reader)
|
|
}
|
|
|
|
pub(crate) fn primop_abort(
|
|
&mut self,
|
|
ctx: &mut impl VmRuntimeCtx,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
// stack: [msg] - force msg, then abort with it
|
|
self.force_slot(0, reader, mc)?;
|
|
let msg_val = self.peek_forced(0);
|
|
let msg = ctx.get_string(msg_val).unwrap_or("<non-string-value>");
|
|
self.finish_err(Error::eval_error(format!(
|
|
"evaluation aborted with the following error message: '{msg}'"
|
|
)))
|
|
}
|
|
|
|
pub(crate) fn primop_deep_seq_force_top(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
// stack: [e1, e2] - force e1, return e2
|
|
self.force_slot(1, reader, mc)?;
|
|
|
|
let e1 = self.peek_forced(1);
|
|
|
|
let children: SmallVec<_> = if let Some(attrs) = e1.as_gc::<AttrSet>() {
|
|
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 = self.pop();
|
|
let _ = self.pop();
|
|
self.push(e2);
|
|
return self.return_from_primop(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 = self.pop();
|
|
let _ = self.pop();
|
|
self.push(e2);
|
|
self.push(Value::new_gc(seen));
|
|
self.push(Value::new_gc(worklist));
|
|
self.push(Value::new_inline(count));
|
|
reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize);
|
|
Step::Continue(())
|
|
}
|
|
|
|
pub(crate) fn primop_deep_seq_push(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
// stack: [e2, seen, worklist, counter]
|
|
#[allow(clippy::unwrap_used)]
|
|
let counter = self.peek(0).as_inline::<i32>().unwrap();
|
|
if counter == 0 {
|
|
let _ = self.pop(); // counter
|
|
let _ = self.pop(); // worklist
|
|
let _ = self.pop(); // seen
|
|
return self.return_from_primop(reader);
|
|
}
|
|
|
|
#[allow(clippy::unwrap_used)]
|
|
let worklist = self.peek_forced(1).as_gc::<List<'gc>>().unwrap();
|
|
#[allow(clippy::unwrap_used)]
|
|
let item = worklist.unlock(mc).borrow_mut().pop().unwrap();
|
|
self.replace(0, Value::new_inline(counter - 1));
|
|
self.push(item);
|
|
|
|
// force item at TOS, resume at DeepSeqLoop after force
|
|
self.force_slot_to_pc(0, reader, mc, PrimOpPhase::DeepSeqLoop.ip() as usize)?;
|
|
reader.set_pc(PrimOpPhase::DeepSeqLoop.ip() as usize);
|
|
Step::Continue(())
|
|
}
|
|
|
|
pub(crate) fn primop_deep_seq_loop(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
// stack after pop: [e2, seen, worklist, counter]
|
|
let item = self.pop();
|
|
#[allow(clippy::unwrap_used)]
|
|
let counter = self.peek(0).as_inline::<i32>().unwrap();
|
|
|
|
let mut added: usize = 0;
|
|
if let Some(attrs) = item.as_gc::<AttrSet>() {
|
|
#[allow(clippy::unwrap_used)]
|
|
let seen = self.peek_forced(2).as_gc::<List<'gc>>().unwrap();
|
|
if !self.is_value_in_seen(seen, item) {
|
|
self.add_value_to_seen(seen, mc, item);
|
|
#[allow(clippy::unwrap_used)]
|
|
let worklist = self.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 = self.peek_forced(2).as_gc::<List<'gc>>().unwrap();
|
|
if !self.is_value_in_seen(seen, item) {
|
|
self.add_value_to_seen(seen, mc, item);
|
|
#[allow(clippy::unwrap_used)]
|
|
let worklist = self.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();
|
|
}
|
|
}
|
|
}
|
|
|
|
self.replace(0, Value::new_inline(counter + added as i32));
|
|
reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize);
|
|
Step::Continue(())
|
|
}
|
|
|
|
pub(crate) fn primop_force_result_shallow(
|
|
&mut self,
|
|
ctx: &mut impl VmRuntimeCtx,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
self.force_slot(0, reader, mc)?;
|
|
let val = self.peek_forced(0);
|
|
|
|
let (count, has_children) = if let Some(attrs) = val.as_gc::<AttrSet>() {
|
|
let len = attrs.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 = self.pop();
|
|
return self.finish_ok(ctx.convert_value(val));
|
|
}
|
|
|
|
self.push(Value::new_inline(0i32));
|
|
self.push(Value::new_inline(count as i32));
|
|
reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize);
|
|
Step::Continue(())
|
|
}
|
|
|
|
pub(crate) fn primop_force_result_shallow_push(
|
|
&mut self,
|
|
ctx: &mut impl VmRuntimeCtx,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
#[allow(clippy::unwrap_used)]
|
|
let idx = self.peek(1).as_inline::<i32>().unwrap();
|
|
#[allow(clippy::unwrap_used)]
|
|
let len = self.peek(0).as_inline::<i32>().unwrap();
|
|
|
|
if idx == len {
|
|
let _ = self.pop(); // len
|
|
let _ = self.pop(); // idx
|
|
let val = self.pop();
|
|
return self.finish_ok(ctx.convert_value(val));
|
|
}
|
|
|
|
let val = self.peek_forced(2);
|
|
let child = if let Some(attrs) = val.as_gc::<AttrSet>() {
|
|
attrs.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 {
|
|
self.replace(1, Value::new_inline(idx + 1));
|
|
self.push(child);
|
|
self.force_slot_to_pc(
|
|
0,
|
|
reader,
|
|
mc,
|
|
PrimOpPhase::ForceResultShallowLoop.ip() as usize,
|
|
)?;
|
|
reader.set_pc(PrimOpPhase::ForceResultShallowLoop.ip() as usize);
|
|
}
|
|
Step::Continue(())
|
|
}
|
|
|
|
pub(crate) fn primop_force_result_shallow_loop(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
_mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
let _ = self.pop(); // forced child
|
|
reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize);
|
|
Step::Continue(())
|
|
}
|
|
|
|
pub(crate) fn primop_force_result_deep_finish(
|
|
&mut self,
|
|
ctx: &mut impl VmRuntimeCtx,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
let val = self.try_force::<StrictValue>(reader, mc)?;
|
|
self.finish_ok(ctx.convert_value(val.relax()))
|
|
}
|
|
|
|
fn is_value_in_seen(&self, 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(&self, seen: Gc<'gc, List<'gc>>, mc: &Mutation<'gc>, val: Value<'gc>) {
|
|
if is_container(val) {
|
|
seen.unlock(mc).borrow_mut().push(val);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_container(val: Value<'_>) -> bool {
|
|
val.is::<AttrSet>() || val.is::<List<'_>>()
|
|
}
|