summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/tinywasm/src/interpreter/executor.rs164
-rw-r--r--crates/tinywasm/src/interpreter/num_helpers.rs23
-rw-r--r--crates/tinywasm/src/interpreter/stack/value_stack.rs55
-rw-r--r--crates/tinywasm/src/interpreter/values.rs27
4 files changed, 95 insertions, 174 deletions
diff --git a/crates/tinywasm/src/interpreter/executor.rs b/crates/tinywasm/src/interpreter/executor.rs
index f739906..794fb3a 100644
--- a/crates/tinywasm/src/interpreter/executor.rs
+++ b/crates/tinywasm/src/interpreter/executor.rs
@@ -50,23 +50,50 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> {
#[inline(always)]
fn exec<const ITERATIONS: usize>(&mut self) -> Result<Option<()>> {
macro_rules! stack_op {
- (simd_unary $method:ident) => { stack_op!(unary Value128, |v| v.$method()) };
- (simd_binary $method:ident) => { stack_op!(binary Value128, |a, b| a.$method(b)) };
- (unary $ty:ty, |$v:ident| $expr:expr) => { self.store.stack.values.unary::<$ty>(|$v| Ok($expr))? };
- (binary $ty:ty, |$a:ident, $b:ident| $expr:expr) => { self.store.stack.values.binary::<$ty>(|$a, $b| Ok($expr))? };
- (binary try $ty:ty, |$a:ident, $b:ident| $expr:expr) => { self.store.stack.values.binary::<$ty>(|$a, $b| $expr)? };
- (unary $from:ty => $to:ty, |$v:ident| $expr:expr) => { self.store.stack.values.unary_into::<$from, $to>(|$v| Ok($expr))? };
- (binary $from:ty => $to:ty, |$a:ident, $b:ident| $expr:expr) => { self.store.stack.values.binary_into::<$from, $to>(|$a, $b| Ok($expr))? };
- (binary_into2 $from:ty => $to:ty, |$a:ident, $b:ident| $expr:expr) => {{
- let $b = self.store.stack.values.pop::<$from>();
- let $a = self.store.stack.values.pop::<$from>();
+ (unary $ty:ty, |$v:ident| $expr:expr) => {{
+ let $v = self.store.stack.values.pop::<$ty>();
+ self.store.stack.values.push::<$ty>($expr)?;
+ }};
+ (binary $ty:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{
+ let $rhs = self.store.stack.values.pop::<$ty>();
+ let $lhs = self.store.stack.values.pop::<$ty>();
+ self.store.stack.values.push::<$ty>($expr)?;
+ }};
+ (binary try $ty:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{
+ let $rhs = self.store.stack.values.pop::<$ty>();
+ let $lhs = self.store.stack.values.pop::<$ty>();
+ self.store.stack.values.push::<$ty>($expr?)?;
+ }};
+ (unary $from:ty => $to:ty, |$v:ident| $expr:expr) => {{
+ let $v = self.store.stack.values.pop::<$from>();
+ self.store.stack.values.push::<$to>($expr)?;
+ }};
+ (binary $from:ty => $to:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{
+ let $rhs = self.store.stack.values.pop::<$from>();
+ let $lhs = self.store.stack.values.pop::<$from>();
+ self.store.stack.values.push::<$to>($expr)?;
+ }};
+ (binary_into2 $from:ty => $to:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{
+ let $rhs = self.store.stack.values.pop::<$from>();
+ let $lhs = self.store.stack.values.pop::<$from>();
let out = $expr;
self.store.stack.values.push::<$to>(out.0)?;
self.store.stack.values.push::<$to>(out.1)?;
}};
- (binary $a:ty, $b:ty, |$lhs:ident, $rhs:ident| $expr:expr) => { stack_op!(binary $a, $b => $b, |$lhs, $rhs| $expr) };
- (binary $a:ty, $b:ty => $res:ty, |$lhs:ident, $rhs:ident| $expr:expr) => { self.store.stack.values.binary_mixed::<$a, $b, $res>(|$lhs, $rhs| Ok($expr))? };
- (ternary $ty:ty, |$a:ident, $b:ident, $c:ident| $expr:expr) => { self.store.stack.values.ternary::<$ty>(|$a, $b, $c| Ok($expr))? };
+ (binary $lhs_ty:ty, $rhs_ty:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {
+ stack_op!(binary $lhs_ty, $rhs_ty => $rhs_ty, |$lhs, $rhs| $expr)
+ };
+ (binary $lhs_ty:ty, $rhs_ty:ty => $res:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{
+ let $rhs = self.store.stack.values.pop::<$rhs_ty>();
+ let $lhs = self.store.stack.values.pop::<$lhs_ty>();
+ self.store.stack.values.push::<$res>($expr)?;
+ }};
+ (ternary $ty:ty, |$a:ident, $b:ident, $c:ident| $expr:expr) => {{
+ let $c = self.store.stack.values.pop::<$ty>();
+ let $b = self.store.stack.values.pop::<$ty>();
+ let $a = self.store.stack.values.pop::<$ty>();
+ self.store.stack.values.push::<$ty>($expr)?;
+ }};
(quaternary_into2 $from:ty => $to:ty, |$a:ident, $b:ident, $c:ident, $d:ident| $expr:expr) => {{
let $d = self.store.stack.values.pop::<$from>();
let $c = self.store.stack.values.pop::<$from>();
@@ -121,39 +148,16 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> {
JumpIfZero(ip) => if self.exec_jump_if_zero(*ip) { continue; },
JumpIfNonZero(ip) => if self.exec_jump_if_non_zero(*ip) { continue; },
DropKeepSmall { base32, keep32, base64, keep64, base128, keep128, base_ref, keep_ref } => {
- let b32 = self.cf.stack_base().s32 + *base32 as u32;
- let k32 = *keep32 as usize;
- self.store.stack.values.stack_32.truncate_keep(b32 as usize, k32);
- let b64 = self.cf.stack_base().s64 + *base64 as u32;
- let k64 = *keep64 as usize;
- self.store.stack.values.stack_64.truncate_keep(b64 as usize, k64);
- let b128 = self.cf.stack_base().s128 + *base128 as u32;
- let k128 = *keep128 as usize;
- self.store.stack.values.stack_128.truncate_keep(b128 as usize, k128);
- let bref = self.cf.stack_base().sref + *base_ref as u32;
- let kref = *keep_ref as usize;
- self.store.stack.values.stack_ref.truncate_keep(bref as usize, kref);
- }
- DropKeep32(base, keep) => {
- let b = self.cf.stack_base().s32 + *base as u32;
- let k = *keep as usize;
- self.store.stack.values.stack_32.truncate_keep(b as usize, k);
- }
- DropKeep64(base, keep) => {
- let b = self.cf.stack_base().s64 + *base as u32;
- let k = *keep as usize;
- self.store.stack.values.stack_64.truncate_keep(b as usize, k);
- }
- DropKeep128(base, keep) => {
- let b = self.cf.stack_base().s128 + *base as u32;
- let k = *keep as usize;
- self.store.stack.values.stack_128.truncate_keep(b as usize, k);
- }
- DropKeepRef(base, keep) => {
- let b = self.cf.stack_base().sref + *base as u32;
- let k = *keep as usize;
- self.store.stack.values.stack_ref.truncate_keep(b as usize, k);
+ let stack_base = self.cf.stack_base();
+ self.store.stack.values.stack_32.truncate_keep((stack_base.s32 + *base32 as u32) as usize, *keep32 as usize);
+ self.store.stack.values.stack_64.truncate_keep((stack_base.s64 + *base64 as u32) as usize, *keep64 as usize);
+ self.store.stack.values.stack_128.truncate_keep((stack_base.s128 + *base128 as u32) as usize, *keep128 as usize);
+ self.store.stack.values.stack_ref.truncate_keep((stack_base.sref + *base_ref as u32) as usize, *keep_ref as usize);
}
+ DropKeep32(base, keep) => self.store.stack.values.stack_32.truncate_keep((self.cf.stack_base().s32 + *base as u32) as usize, *keep as usize),
+ DropKeep64(base, keep) => self.store.stack.values.stack_64.truncate_keep((self.cf.stack_base().s64 + *base as u32) as usize, *keep as usize),
+ DropKeep128(base, keep) => self.store.stack.values.stack_128.truncate_keep((self.cf.stack_base().s128 + *base as u32) as usize, *keep as usize),
+ DropKeepRef(base, keep) => self.store.stack.values.stack_ref.truncate_keep((self.cf.stack_base().sref + *base as u32) as usize, *keep as usize),
BranchTable(default_ip, start, len) => { self.exec_branch_table(*default_ip, *start, *len); continue; }
Return => { if self.exec_return() { return Ok(Some(())); } continue; }
LocalGet32(local_index) => self.store.stack.values.push(self.store.stack.values.local_get::<Value32>(&self.cf, *local_index))?,
@@ -412,7 +416,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> {
V128AndNot => stack_op!(binary Value128, |a, b| a.v128_andnot(b)),
V128Or => stack_op!(binary Value128, |a, b| a.v128_or(b)),
V128Xor => stack_op!(binary Value128, |a, b| a.v128_xor(b)),
- V128Bitselect => stack_op!(ternary Value128, |v1, v2, c| Value128::v128_bitselect(v1, v2, c)),
+ V128Bitselect => stack_op!(ternary Value128, |a, b, c| Value128::v128_bitselect(a, b, c)),
V128AnyTrue => stack_op!(unary Value128 => i32, |v| v.v128_any_true() as i32),
I8x16Swizzle => stack_op!(binary Value128, |a, s| a.i8x16_swizzle(s)),
I8x16RelaxedSwizzle => stack_op!(binary Value128, |a, s| a.i8x16_relaxed_swizzle(s)),
@@ -604,43 +608,43 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> {
I8x16Shuffle(idx) => { let idx = self.func.data.v128_constants[*idx as usize].to_le_bytes(); stack_op!(binary Value128, |a, b| Value128::i8x16_shuffle(a, b, idx)) }
I16x8Q15MulrSatS => stack_op!(binary Value128, |a, b| a.i16x8_q15mulr_sat_s(b)),
I32x4DotI16x8S => stack_op!(binary Value128, |a, b| a.i32x4_dot_i16x8_s(b)),
- I8x16RelaxedLaneselect => stack_op!(ternary Value128, |v1, v2, c| Value128::i8x16_relaxed_laneselect(v1, v2, c)),
- I16x8RelaxedLaneselect => stack_op!(ternary Value128, |v1, v2, c| Value128::i16x8_relaxed_laneselect(v1, v2, c)),
- I32x4RelaxedLaneselect => stack_op!(ternary Value128, |v1, v2, c| Value128::i32x4_relaxed_laneselect(v1, v2, c)),
- I64x2RelaxedLaneselect => stack_op!(ternary Value128, |v1, v2, c| Value128::i64x2_relaxed_laneselect(v1, v2, c)),
+ I8x16RelaxedLaneselect => stack_op!(ternary Value128, |a, b, c| Value128::i8x16_relaxed_laneselect(a, b, c)),
+ I16x8RelaxedLaneselect => stack_op!(ternary Value128, |a, b, c| Value128::i16x8_relaxed_laneselect(a, b, c)),
+ I32x4RelaxedLaneselect => stack_op!(ternary Value128, |a, b, c| Value128::i32x4_relaxed_laneselect(a, b, c)),
+ I64x2RelaxedLaneselect => stack_op!(ternary Value128, |a, b, c| Value128::i64x2_relaxed_laneselect(a, b, c)),
I16x8RelaxedQ15mulrS => stack_op!(binary Value128, |a, b| a.i16x8_relaxed_q15mulr_s(b)),
I16x8RelaxedDotI8x16I7x16S => stack_op!(binary Value128, |a, b| a.i16x8_relaxed_dot_i8x16_i7x16_s(b)),
I32x4RelaxedDotI8x16I7x16AddS => stack_op!(ternary Value128, |a, b, c| a.i32x4_relaxed_dot_i8x16_i7x16_add_s(b, c)),
- F32x4Ceil => stack_op!(simd_unary f32x4_ceil),
- F64x2Ceil => stack_op!(simd_unary f64x2_ceil),
- F32x4Floor => stack_op!(simd_unary f32x4_floor),
- F64x2Floor => stack_op!(simd_unary f64x2_floor),
- F32x4Trunc => stack_op!(simd_unary f32x4_trunc),
- F64x2Trunc => stack_op!(simd_unary f64x2_trunc),
- F32x4Nearest => stack_op!(simd_unary f32x4_nearest),
- F64x2Nearest => stack_op!(simd_unary f64x2_nearest),
- F32x4Abs => stack_op!(simd_unary f32x4_abs),
- F64x2Abs => stack_op!(simd_unary f64x2_abs),
- F32x4Neg => stack_op!(simd_unary f32x4_neg),
- F64x2Neg => stack_op!(simd_unary f64x2_neg),
- F32x4Sqrt => stack_op!(simd_unary f32x4_sqrt),
- F64x2Sqrt => stack_op!(simd_unary f64x2_sqrt),
- F32x4Add => stack_op!(simd_binary f32x4_add),
- F64x2Add => stack_op!(simd_binary f64x2_add),
- F32x4Sub => stack_op!(simd_binary f32x4_sub),
- F64x2Sub => stack_op!(simd_binary f64x2_sub),
- F32x4Mul => stack_op!(simd_binary f32x4_mul),
- F64x2Mul => stack_op!(simd_binary f64x2_mul),
- F32x4Div => stack_op!(simd_binary f32x4_div),
- F64x2Div => stack_op!(simd_binary f64x2_div),
- F32x4Min => stack_op!(simd_binary f32x4_min),
- F64x2Min => stack_op!(simd_binary f64x2_min),
- F32x4Max => stack_op!(simd_binary f32x4_max),
- F64x2Max => stack_op!(simd_binary f64x2_max),
- F32x4PMin => stack_op!(simd_binary f32x4_pmin),
- F32x4PMax => stack_op!(simd_binary f32x4_pmax),
- F64x2PMin => stack_op!(simd_binary f64x2_pmin),
- F64x2PMax => stack_op!(simd_binary f64x2_pmax),
+ F32x4Ceil => stack_op!(unary Value128, |v| v.f32x4_ceil()),
+ F64x2Ceil => stack_op!(unary Value128, |v| v.f64x2_ceil()),
+ F32x4Floor => stack_op!(unary Value128, |v| v.f32x4_floor()),
+ F64x2Floor => stack_op!(unary Value128, |v| v.f64x2_floor()),
+ F32x4Trunc => stack_op!(unary Value128, |v| v.f32x4_trunc()),
+ F64x2Trunc => stack_op!(unary Value128, |v| v.f64x2_trunc()),
+ F32x4Nearest => stack_op!(unary Value128, |v| v.f32x4_nearest()),
+ F64x2Nearest => stack_op!(unary Value128, |v| v.f64x2_nearest()),
+ F32x4Abs => stack_op!(unary Value128, |v| v.f32x4_abs()),
+ F64x2Abs => stack_op!(unary Value128, |v| v.f64x2_abs()),
+ F32x4Neg => stack_op!(unary Value128, |v| v.f32x4_neg()),
+ F64x2Neg => stack_op!(unary Value128, |v| v.f64x2_neg()),
+ F32x4Sqrt => stack_op!(unary Value128, |v| v.f32x4_sqrt()),
+ F64x2Sqrt => stack_op!(unary Value128, |v| v.f64x2_sqrt()),
+ F32x4Add => stack_op!(binary Value128, |a, b| a.f32x4_add(b)),
+ F64x2Add => stack_op!(binary Value128, |a, b| a.f64x2_add(b)),
+ F32x4Sub => stack_op!(binary Value128, |a, b| a.f32x4_sub(b)),
+ F64x2Sub => stack_op!(binary Value128, |a, b| a.f64x2_sub(b)),
+ F32x4Mul => stack_op!(binary Value128, |a, b| a.f32x4_mul(b)),
+ F64x2Mul => stack_op!(binary Value128, |a, b| a.f64x2_mul(b)),
+ F32x4Div => stack_op!(binary Value128, |a, b| a.f32x4_div(b)),
+ F64x2Div => stack_op!(binary Value128, |a, b| a.f64x2_div(b)),
+ F32x4Min => stack_op!(binary Value128, |a, b| a.f32x4_min(b)),
+ F64x2Min => stack_op!(binary Value128, |a, b| a.f64x2_min(b)),
+ F32x4Max => stack_op!(binary Value128, |a, b| a.f32x4_max(b)),
+ F64x2Max => stack_op!(binary Value128, |a, b| a.f64x2_max(b)),
+ F32x4PMin => stack_op!(binary Value128, |a, b| a.f32x4_pmin(b)),
+ F32x4PMax => stack_op!(binary Value128, |a, b| a.f32x4_pmax(b)),
+ F64x2PMin => stack_op!(binary Value128, |a, b| a.f64x2_pmin(b)),
+ F64x2PMax => stack_op!(binary Value128, |a, b| a.f64x2_pmax(b)),
F32x4RelaxedMadd => stack_op!(ternary Value128, |a, b, c| a.f32x4_relaxed_madd(b, c)),
F32x4RelaxedNmadd => stack_op!(ternary Value128, |a, b, c| a.f32x4_relaxed_nmadd(b, c)),
F64x2RelaxedMadd => stack_op!(ternary Value128, |a, b, c| a.f64x2_relaxed_madd(b, c)),
diff --git a/crates/tinywasm/src/interpreter/num_helpers.rs b/crates/tinywasm/src/interpreter/num_helpers.rs
index 5ae81ef..20882b3 100644
--- a/crates/tinywasm/src/interpreter/num_helpers.rs
+++ b/crates/tinywasm/src/interpreter/num_helpers.rs
@@ -30,18 +30,17 @@ macro_rules! checked_conv_float {
checked_conv_float!($from, $to, $to, $self)
};
// Conversion with an intermediate unsigned type and error checking (three types)
- ($from:tt, $intermediate:tt, $to:tt, $self:expr) => {
- $self.store.stack.values.unary_into::<$from, $to>(|v| {
- let (min, max) = float_min_max!($from, $intermediate);
- if unlikely(v.is_nan()) {
- return Err(Error::Trap(crate::Trap::InvalidConversionToInt));
- }
- if unlikely(v <= min || v >= max) {
- return Err(Error::Trap(crate::Trap::IntegerOverflow));
- }
- Ok((v as $intermediate as $to).into())
- })?
- };
+ ($from:tt, $intermediate:tt, $to:tt, $self:expr) => {{
+ let v = $self.store.stack.values.pop::<$from>();
+ let (min, max) = float_min_max!($from, $intermediate);
+ if unlikely(v.is_nan()) {
+ return Err(Error::Trap(crate::Trap::InvalidConversionToInt));
+ }
+ if unlikely(v <= min || v >= max) {
+ return Err(Error::Trap(crate::Trap::IntegerOverflow));
+ }
+ $self.store.stack.values.push::<$to>((v as $intermediate as $to).into())?;
+ }};
}
pub(crate) use checked_conv_float;
diff --git a/crates/tinywasm/src/interpreter/stack/value_stack.rs b/crates/tinywasm/src/interpreter/stack/value_stack.rs
index b67ab09..2b13b67 100644
--- a/crates/tinywasm/src/interpreter/stack/value_stack.rs
+++ b/crates/tinywasm/src/interpreter/stack/value_stack.rs
@@ -59,14 +59,6 @@ impl<T: Copy + Default> Stack<T> {
}
#[inline(always)]
- pub(crate) fn last_mut(&mut self) -> &mut T {
- if self.len == 0 {
- unreachable!("ValueStack underflow, this is a bug");
- }
- &mut self.data[self.len - 1]
- }
-
- #[inline(always)]
pub(crate) fn get(&self, index: usize) -> T {
match self.data.get(index) {
Some(v) => *v,
@@ -203,53 +195,6 @@ impl ValueStack {
self.stack_ref.select_many(counts.cref as usize, condition);
}
- #[inline(always)]
- pub(crate) fn unary<T: InternalValue>(&mut self, func: impl FnOnce(T) -> Result<T>) -> Result<()> {
- T::stack_apply1(self, func)
- }
-
- #[inline(always)]
- pub(crate) fn unary_into<IN: InternalValue, OUT: InternalValue>(
- &mut self,
- func: impl FnOnce(IN) -> Result<OUT>,
- ) -> Result<()> {
- let v = IN::stack_pop(self);
- OUT::stack_push(self, func(v)?)?;
- Ok(())
- }
-
- #[inline(always)]
- pub(crate) fn binary<T: InternalValue>(&mut self, func: impl FnOnce(T, T) -> Result<T>) -> Result<()> {
- T::stack_apply2(self, func)
- }
-
- #[inline(always)]
- pub(crate) fn binary_into<IN: InternalValue, OUT: InternalValue>(
- &mut self,
- func: impl FnOnce(IN, IN) -> Result<OUT>,
- ) -> Result<()> {
- let rhs = IN::stack_pop(self);
- let lhs = IN::stack_pop(self);
- OUT::stack_push(self, func(lhs, rhs)?)?;
- Ok(())
- }
-
- #[inline(always)]
- pub(crate) fn binary_mixed<A: InternalValue, B: InternalValue, OUT: InternalValue>(
- &mut self,
- func: impl FnOnce(A, B) -> Result<OUT>,
- ) -> Result<()> {
- let rhs = B::stack_pop(self);
- let lhs = A::stack_pop(self);
- OUT::stack_push(self, func(lhs, rhs)?)?;
- Ok(())
- }
-
- #[inline(always)]
- pub(crate) fn ternary<T: InternalValue>(&mut self, func: impl FnOnce(T, T, T) -> Result<T>) -> Result<()> {
- T::stack_apply3(self, func)
- }
-
pub(crate) fn pop_types<'a>(
&'a mut self,
val_types: impl IntoIterator<Item = &'a ValType>,
diff --git a/crates/tinywasm/src/interpreter/values.rs b/crates/tinywasm/src/interpreter/values.rs
index ec68035..54ba00f 100644
--- a/crates/tinywasm/src/interpreter/values.rs
+++ b/crates/tinywasm/src/interpreter/values.rs
@@ -111,9 +111,6 @@ pub(crate) trait InternalValue: sealed::Sealed + Into<TinyWasmValue> + Copy + De
fn local_set(stack: &mut ValueStack, frame: &CallFrame, index: LocalAddr, value: Self);
fn stack_pop(stack: &mut ValueStack) -> Self;
fn stack_peek(stack: &ValueStack) -> Self;
- fn stack_apply1(stack: &mut ValueStack, func: impl FnOnce(Self) -> Result<Self>) -> Result<()>;
- fn stack_apply2(stack: &mut ValueStack, func: impl FnOnce(Self, Self) -> Result<Self>) -> Result<()>;
- fn stack_apply3(stack: &mut ValueStack, func: impl FnOnce(Self, Self, Self) -> Result<Self>) -> Result<()>;
}
macro_rules! impl_internalvalue {
@@ -160,30 +157,6 @@ macro_rules! impl_internalvalue {
fn stack_peek(stack: &ValueStack) -> Self {
$to_outer(*stack.$stack.last())
}
-
- #[inline(always)]
- fn stack_apply1(stack: &mut ValueStack, func: impl FnOnce(Self) -> Result<Self>) -> Result<()> {
- let top = stack.$stack.last_mut();
- *top = $to_internal(func($to_outer(*top))?);
- Ok(())
- }
-
- #[inline(always)]
- fn stack_apply2(stack: &mut ValueStack, func: impl FnOnce(Self, Self) -> Result<Self>) -> Result<()> {
- let v2 = stack.$stack.pop();
- let v1 = stack.$stack.last_mut();
- *v1 = $to_internal(func($to_outer(*v1), $to_outer(v2))?);
- Ok(())
- }
-
- #[inline(always)]
- fn stack_apply3(stack: &mut ValueStack, func: impl FnOnce(Self, Self, Self) -> Result<Self>) -> Result<()> {
- let v3 = stack.$stack.pop();
- let v2 = stack.$stack.pop();
- let v1 = stack.$stack.last_mut();
- *v1 = $to_internal(func($to_outer(*v1), $to_outer(v2), $to_outer(v3))?);
- Ok(())
- }
}
)*
};