use fix_builtins::PrimOpPhase; use fix_error::Error; use gc_arena::{Gc, Mutation, RefLock}; 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), CallPattern => self.primop_call_pattern(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::() { 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::().unwrap(); #[allow(clippy::unwrap_used)] let elem = self.peek_forced(2).as_gc::().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::(reader, mc)?; #[allow(clippy::unwrap_used)] let idx = self.peek(1).as_inline::().unwrap(); #[allow(clippy::unwrap_used)] let list = self.peek_forced(2).as_gc::().unwrap(); let list = list.inner.borrow(); #[allow(clippy::unwrap_used)] let acc = self.peek_forced(0).as_gc::().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(""); 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::() { 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::>() { 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::().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::>().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::().unwrap(); let mut added: usize = 0; if let Some(attrs) = item.as_gc::() { let attrs = &attrs.entries; #[allow(clippy::unwrap_used)] let seen = self.peek_forced(2).as_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::>().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::>() { #[allow(clippy::unwrap_used)] let seen = self.peek_forced(2).as_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::>().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::() { let len = attrs.entries.len(); (len, len > 0) } else if let Some(list) = val.as_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::().unwrap(); #[allow(clippy::unwrap_used)] let len = self.peek(0).as_inline::().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::() { attrs.entries.get(idx as usize).map(|&(_, v)| v) } else if let Some(list) = val.as_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::(reader, mc)?; self.finish_ok(ctx.convert_value(val.relax())) } pub(crate) fn primop_call_pattern( &mut self, ctx: &mut impl VmRuntimeCtx, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { let (func, attrset) = self.try_force::<(Gc, Gc)>(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 self.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 self.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); self.env = new_env; Step::Continue(()) } 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::() || val.is::>() }