diff --git a/fix-vm/src/instructions/arithmetic.rs b/fix-vm/src/instructions/arithmetic.rs index 9c719a3..a4f110c 100644 --- a/fix-vm/src/instructions/arithmetic.rs +++ b/fix-vm/src/instructions/arithmetic.rs @@ -13,7 +13,7 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; + let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string(rhs)) { let ns = Gc::new(mc, crate::NixString::new(format!("{ls}{rs}"))); self.push(Value::new_gc(ns)); @@ -47,7 +47,7 @@ impl<'gc> crate::Vm<'gc> { int_op: fn(i64, i64) -> i64, float_op: fn(f64, f64) -> f64, ) -> Step { - let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; + let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let res = numeric_binop(lhs, rhs, mc, int_op, float_op); match res { Ok(val) => { @@ -60,7 +60,7 @@ impl<'gc> crate::Vm<'gc> { #[inline(always)] pub(crate) fn op_div(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step { - let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; + let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; match (get_num(lhs), get_num(rhs)) { (_, Some(NixNum::Int(0))) | (_, Some(NixNum::Float(0.))) => { return self.finish_vm_err(VmError::Uncatchable(fix_error::Error::eval_error( @@ -86,7 +86,7 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; + let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let eq = match self.values_equal(ctx, lhs, rhs) { Ok(eq) => eq, Err(e) => return self.finish_vm_err(e), @@ -102,7 +102,7 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; + let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let eq = match self.values_equal(ctx, lhs, rhs) { Ok(eq) => eq, Err(e) => return self.finish_vm_err(e), @@ -158,7 +158,7 @@ impl<'gc> crate::Vm<'gc> { mc: &Mutation<'gc>, pred: fn(Ordering) -> bool, ) -> Step { - let (lhs, rhs) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; + let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; match self.compare_values_inner(ctx, pred, lhs, rhs) { Ok(()) => Step::Continue(()), Err(e) => self.finish_vm_err(e), @@ -171,7 +171,7 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let (l, r) = self.try_force::<(Gc, Gc)>(reader, mc)?; + let (l, r) = self.force_and_retry::<(Gc, Gc)>(reader, mc)?; let mut items = smallvec::SmallVec::new(); items.extend_from_slice(&l.inner.borrow()); items.extend_from_slice(&r.inner.borrow()); @@ -190,14 +190,14 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let (l, r) = self.try_force::<(Gc, Gc)>(reader, mc)?; + let (l, r) = self.force_and_retry::<(Gc, Gc)>(reader, mc)?; self.push(Value::new_gc(l.merge(&r, mc))); Step::Continue(()) } #[inline(always)] pub(crate) fn op_neg(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step { - let rhs = self.try_force::(reader, mc)?; + let rhs = self.force_and_retry::(reader, mc)?; match rhs { NixNum::Int(int) => self.push(Value::make_int(-int, mc)), NixNum::Float(float) => self.push(Value::new_float(-float)), @@ -207,7 +207,7 @@ impl<'gc> crate::Vm<'gc> { #[inline(always)] pub(crate) fn op_not(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step { - let rhs = self.try_force::(reader, mc)?; + let rhs = self.force_and_retry::(reader, mc)?; self.push(Value::new_inline(!rhs)); Step::Continue(()) } diff --git a/fix-vm/src/instructions/builtins.rs b/fix-vm/src/instructions/builtins.rs index 92956b3..a460896 100644 --- a/fix-vm/src/instructions/builtins.rs +++ b/fix-vm/src/instructions/builtins.rs @@ -101,7 +101,7 @@ impl<'gc> Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let ret = self.try_force::(reader, mc)?; + let ret = self.force_and_retry::(reader, mc)?; #[allow(clippy::unwrap_used)] let idx = self.peek(1).as_inline::().unwrap(); #[allow(clippy::unwrap_used)] @@ -368,7 +368,7 @@ impl<'gc> Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let val = self.try_force::(reader, mc)?; + let val = self.force_and_retry::(reader, mc)?; self.finish_ok(ctx.convert_value(val.relax())) } @@ -378,7 +378,7 @@ impl<'gc> Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let (func, attrset) = self.try_force::<(Gc, Gc)>(reader, mc)?; + let (func, attrset) = self.force_and_retry::<(Gc, Gc)>(reader, mc)?; let Closure { ip, diff --git a/fix-vm/src/instructions/calls.rs b/fix-vm/src/instructions/calls.rs index 78a59ad..b6b4b8a 100644 --- a/fix-vm/src/instructions/calls.rs +++ b/fix-vm/src/instructions/calls.rs @@ -17,7 +17,7 @@ impl<'gc> crate::Vm<'gc> { arg: Value<'gc>, resume_pc: usize, ) -> Step { - let func = self.try_force::(reader, mc)?; + let func = self.force_and_retry::(reader, mc)?; if self.call_depth > 10000 { return self.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded")); } @@ -118,7 +118,7 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let val = self.try_force::(reader, mc)?; + let val = self.force_and_retry::(reader, mc)?; let Some(CallFrame { pc: ret_pc, stack_depth, diff --git a/fix-vm/src/instructions/collections.rs b/fix-vm/src/instructions/collections.rs index 66670f4..f9b910e 100644 --- a/fix-vm/src/instructions/collections.rs +++ b/fix-vm/src/instructions/collections.rs @@ -75,7 +75,7 @@ impl<'gc> crate::Vm<'gc> { let _span_id = reader.read_u32(); let key = reader.read_string_id(); - let attrset = self.try_force::>(reader, mc)?; + let attrset = self.force_and_retry::>(reader, mc)?; match attrset.lookup(key) { Some(v) => { @@ -95,7 +95,7 @@ impl<'gc> crate::Vm<'gc> { ) -> Step { let _span_id = reader.read_u32(); - let (attrset, key_val) = self.try_force::<(Gc, StrictValue)>(reader, mc)?; + let (attrset, key_val) = self.force_and_retry::<(Gc, StrictValue)>(reader, mc)?; let key_sid = match ctx.get_string_id(key_val) { Ok(id) => id, @@ -191,7 +191,7 @@ impl<'gc> crate::Vm<'gc> { let _span_id = reader.read_u32(); let key = reader.read_string_id(); - let current = self.try_force::(reader, mc)?; + let current = self.force_and_retry::(reader, mc)?; match current .as_gc::() @@ -214,7 +214,7 @@ impl<'gc> crate::Vm<'gc> { ) -> Step { let _span_id = reader.read_u32(); - let (current, key_val) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; + let (current, key_val) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let key_sid = match ctx.get_string_id(key_val) { Ok(id) => id, @@ -254,7 +254,7 @@ impl<'gc> crate::Vm<'gc> { mc: &gc_arena::Mutation<'gc>, ) -> Step { let key = reader.read_string_id(); - let current = self.try_force::(reader, mc)?; + let current = self.force_and_retry::(reader, mc)?; self.push(Value::new_inline( current @@ -275,7 +275,7 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { - let (current, dyn_key) = self.try_force::<(StrictValue, StrictValue)>(reader, mc)?; + let (current, dyn_key) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; let key_sid = match ctx.get_string_id(dyn_key) { Ok(id) => id, diff --git a/fix-vm/src/instructions/control.rs b/fix-vm/src/instructions/control.rs index 605269b..9dd02d4 100644 --- a/fix-vm/src/instructions/control.rs +++ b/fix-vm/src/instructions/control.rs @@ -9,7 +9,7 @@ impl<'gc> crate::Vm<'gc> { mc: &gc_arena::Mutation<'gc>, ) -> Step { let offset = reader.read_i32(); - let cond = self.try_force::(reader, mc)?; + let cond = self.force_and_retry::(reader, mc)?; if cond.as_inline::() == Some(false) { reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); } @@ -23,7 +23,7 @@ impl<'gc> crate::Vm<'gc> { mc: &gc_arena::Mutation<'gc>, ) -> Step { let offset = reader.read_i32(); - let cond = self.try_force::(reader, mc)?; + let cond = self.force_and_retry::(reader, mc)?; if cond.as_inline::() == Some(true) { reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); } diff --git a/fix-vm/src/instructions/misc.rs b/fix-vm/src/instructions/misc.rs index 258c58c..c3c246c 100644 --- a/fix-vm/src/instructions/misc.rs +++ b/fix-vm/src/instructions/misc.rs @@ -42,7 +42,7 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &gc_arena::Mutation<'gc>, ) -> Step { - let val = self.try_force::(reader, mc)?; + let val = self.force_and_retry::(reader, mc)?; if val.is::() || val.is::() { self.push(val.relax()); } else { diff --git a/fix-vm/src/instructions/with_scope.rs b/fix-vm/src/instructions/with_scope.rs index be881a8..58b1002 100644 --- a/fix-vm/src/instructions/with_scope.rs +++ b/fix-vm/src/instructions/with_scope.rs @@ -66,7 +66,7 @@ impl<'gc> crate::Vm<'gc> { ))); }; self.push(env); - let env = self.try_force::>(reader, mc)?; + let env = self.force_and_retry::>(reader, mc)?; let Some(val) = env.lookup(name) else { reader.set_pc(reader.inst_start_pc()); self.with_env = prev; diff --git a/fix-vm/src/lib.rs b/fix-vm/src/lib.rs index 5ba903a..cdd3827 100644 --- a/fix-vm/src/lib.rs +++ b/fix-vm/src/lib.rs @@ -420,17 +420,45 @@ impl<'gc> Vm<'gc> { .expect("forced") } + /// Force the top `T::WIDTH` stack slots and return them as `T`. + /// + /// If any slot holds a pending thunk, this method pushes a call frame + /// whose resume PC is the **start of the current instruction** + /// (`reader.inst_start_pc()`), enters the thunk, and returns + /// `Break::Force`. When the thunk eventually returns, the VM will + /// **re-execute the entire opcode handler from the beginning**. + /// + /// # Invariants + /// + /// * **Do not call this method more than once in a single handler.** + /// If you need to force multiple values, use a tuple type such as + /// `(StrictValue, StrictValue)` so they are forced and popped in one + /// atomic operation. Calling `force_and_retry` twice (or more) + /// means the handler will be re-run from the top after each retry; + /// any stack modifications between the two calls would be duplicated + /// and corrupt the stack layout. + /// + /// * The caller must ensure that the stack layout at the point of + /// invocation is **identical** every time the handler is re-entered. + /// In practice this means no pushes, pops, or local mutations may + /// happen before the call, and the call must be the first thing + /// that consumes the instruction's operand values. + /// + /// * The return value must be propagated with `?` so that + /// `Break::Force` correctly unwinds to the dispatch loop. #[inline(always)] - pub(crate) fn try_force>( + pub(crate) fn force_and_retry>( &mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> std::ops::ControlFlow { - self.try_force_to_pc(reader, mc, reader.inst_start_pc()) + self.force_and_retry_pc(reader, mc, reader.inst_start_pc()) } + /// Same as [`force_and_retry`](Self::force_and_retry) but allows + /// specifying a custom resume PC. #[inline(always)] - pub(crate) fn try_force_to_pc>( + pub(crate) fn force_and_retry_pc>( &mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>,