From 53330ddee0a3965e368d100e14222e3e9ae5a8b3 Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Mon, 10 Nov 2025 17:40:52 -0700 Subject: [PATCH 1/9] Stack fixup for new types --- crates/fuzzing/src/generators/gc_ops/ops.rs | 198 ++++++++++-------- crates/fuzzing/src/generators/gc_ops/tests.rs | 2 +- crates/fuzzing/src/generators/gc_ops/types.rs | 78 +++++++ 3 files changed, 189 insertions(+), 89 deletions(-) diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index 4933e2c36c34..b743d9653fd4 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -1,12 +1,12 @@ //! Operations for the `gc` operations. +use crate::generators::gc_ops::types::StackType; use crate::generators::gc_ops::{ limits::GcOpsLimits, types::{CompositeType, RecGroupId, StructType, TypeId, Types}, }; use mutatis::{Context, Generate, mutators as m}; use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; use std::collections::BTreeMap; use wasm_encoder::{ CodeSection, ConstExpr, EntityType, ExportKind, ExportSection, Function, FunctionSection, @@ -245,7 +245,8 @@ impl GcOps { self.types.fixup(&self.limits); let mut new_ops = Vec::with_capacity(self.ops.len()); - let mut stack = 0; + let mut stack: Vec = Vec::new(); + let num_types = self.types.type_defs.len() as u32; for mut op in self.ops.iter().copied() { if self.limits.max_types == 0 @@ -268,24 +269,27 @@ impl GcOps { op.fixup(&self.limits); - let mut temp = SmallVec::<[_; 4]>::new(); - - while stack < op.operands_len() { - temp.push(GcOp::Null()); - stack += 1; + match &mut op { + GcOp::StructNew(t) | GcOp::TakeStructCall(t) | GcOp::TakeTypedStructCall(t) => { + if num_types > 0 { + *t = *t % num_types; + } + } + _ => {} } - temp.push(op); - stack = stack - op.operands_len() + op.results_len(); + op.operand_types().iter().for_each(|ty| { + StackType::fixup_stack_type(*ty, &mut stack, &mut new_ops, num_types); + }); - new_ops.extend(temp); + // Finally, emit the op itself (updates stack abstractly) + StackType::emit(op, &mut stack, &mut new_ops, num_types); } - // Insert drops to balance the final stack state - for _ in 0..stack { + // Balance any leftovers with drops (works for any type) + for _ in 0..stack.len() { new_ops.push(GcOp::Drop()); } - self.ops = new_ops; } @@ -301,79 +305,68 @@ impl GcOps { macro_rules! define_gc_ops { ( $( - $op:ident $( ( $($limit_var:ident : $limit:expr => $ty:ty),* ) )? : $params:expr => $results:expr , + $op:ident + $( ( $($limit_var:ident : $limit:expr => $ty:ty),* ) )? + : ($operand_req:expr, $params:expr) => ($result_type:expr, $results:expr) , )* ) => { - /// The operations for the `gc` operations. #[derive(Copy, Clone, Debug, Serialize, Deserialize)] - pub(crate) enum GcOp { + /// The operations that can be performed by the `gc` function. + pub enum GcOp { $( + #[allow(missing_docs, reason = "macro-generated code")] $op ( $( $($ty),* )? ), )* } - /// Names of the operations for testing purposes. #[cfg(test)] - pub const OP_NAMES: &'static[&'static str] = &[ - $( - stringify!($op), - )* + pub(crate) const OP_NAMES: &'static [&'static str] = &[ + $( stringify!($op), )* ]; impl GcOp { #[cfg(test)] - pub fn name(&self) -> &'static str { - match self { - $( - Self::$op (..) => stringify!($op), - )* - } + pub(crate) fn name(&self) -> &'static str { + match self { $( Self::$op(..) => stringify!($op), )* } } - pub fn operands_len(&self) -> usize { - match self { - $( - Self::$op (..) => $params, - )* - } + pub(crate) fn operands_len(&self) -> usize { + match self { $( Self::$op(..) => $params, )* } } - pub fn results_len(&self) -> usize { + #[allow(unreachable_patterns, reason = "macro-generated code")] + pub(crate) fn operand_types(&self) -> Vec { match self { + // special-cases + Self::TakeTypedStructCall(t) => vec![StackType::Struct(Some(*t))], $( - Self::$op (..) => $results, - )* + Self::$op(..) => match $operand_req { + None => vec![], + Some(req) => if $params == 0 { vec![] } else { vec![req; $params] }, + } + ),* } } - } - $( - #[allow(non_snake_case, reason = "macro-generated code")] - fn $op( - _ctx: &mut mutatis::Context, - _limits: &GcOpsLimits, - stack: usize, - ) -> mutatis::Result<(GcOp, usize)> { - #[allow(unused_comparisons, reason = "macro-generated code")] - { - debug_assert!(stack >= $params); - } + pub(crate) fn results_len(&self) -> usize { + match self { $( Self::$op(..) => $results, )* } + } - let op = GcOp::$op( - $($({ - let limit_fn = $limit as fn(&GcOpsLimits) -> $ty; - let limit = (limit_fn)(_limits); - debug_assert!(limit > 0); - m::range(0..=limit - 1).generate(_ctx)? - }),*)? - ); - let new_stack = stack - $params + $results; - Ok((op, new_stack)) + #[allow(unreachable_patterns, reason = "macro-generated code")] + pub(crate) fn result_types(&self) -> Vec { + match self { + // special-cases + Self::StructNew(t) => vec![StackType::Struct(Some(*t))], + $( + Self::$op(..) => match $result_type { + None => vec![], + Some(ty) => if $results == 0 { vec![] } else { vec![ty; $results] }, + } + ),* + } } - )* - impl GcOp { - fn fixup(&mut self, limits: &GcOpsLimits) { + pub(crate) fn fixup(&mut self, limits: &GcOpsLimits) { match self { $( Self::$op( $( $( $limit_var ),* )? ) => { @@ -396,13 +389,13 @@ macro_rules! define_gc_ops { let mut valid_choices: Vec< fn(&mut Context, &GcOpsLimits, usize) -> mutatis::Result<(GcOp, usize)> > = vec![]; + $( #[allow(unused_comparisons, reason = "macro-generated code")] if stack >= $params $($( && { let limit_fn = $limit as fn(&GcOpsLimits) -> $ty; - let limit = (limit_fn)(&ops.limits); - limit > 0 + (limit_fn)(&ops.limits) > 0 } )*)? { valid_choices.push($op); @@ -412,36 +405,69 @@ macro_rules! define_gc_ops { let f = *ctx.rng() .choose(&valid_choices) .expect("should always have a valid op choice"); - (f)(ctx, &ops.limits, stack) } } + + $( + #[allow(non_snake_case, reason = "macro-generated code")] + fn $op( + _ctx: &mut mutatis::Context, + _limits: &GcOpsLimits, + stack: usize, + ) -> mutatis::Result<(GcOp, usize)> { + #[allow(unused_comparisons, reason = "macro-generated code")] + { debug_assert!(stack >= $params); } + + let op = GcOp::$op( + $($({ + let limit_fn = $limit as fn(&GcOpsLimits) -> $ty; + let limit = (limit_fn)(_limits); + debug_assert!(limit > 0); + m::range(0..=limit - 1).generate(_ctx)? + }),*)? + ); + Ok((op, stack - $params + $results)) + } + )* }; } define_gc_ops! { - Gc : 0 => 3, + Gc : (None, 0) => (Some(StackType::ExternRef), 3), - MakeRefs : 0 => 3, - TakeRefs : 3 => 0, + MakeRefs : (None, 0) => (Some(StackType::ExternRef), 3), + TakeRefs : (Some(StackType::ExternRef), 3) => (None, 0), // Add one to make sure that out of bounds table accesses are possible, but still rare. - TableGet(elem_index: |ops| ops.table_size + 1 => u32) : 0 => 1, - TableSet(elem_index: |ops| ops.table_size + 1 => u32) : 1 => 0, - - GlobalGet(global_index: |ops| ops.num_globals => u32) : 0 => 1, - GlobalSet(global_index: |ops| ops.num_globals => u32) : 1 => 0, - - LocalGet(local_index: |ops| ops.num_params => u32) : 0 => 1, - LocalSet(local_index: |ops| ops.num_params => u32) : 1 => 0, - - StructNew(type_index: |ops| ops.max_types => u32) : 0 => 0, - TakeStructCall(type_index: |ops| ops.max_types => u32) : 1 => 0, - TakeTypedStructCall(type_index: |ops| ops.max_types => u32) : 1 => 0, - - Drop : 1 => 0, - - Null : 0 => 1, + TableGet(elem_index: |ops| ops.table_size + 1 => u32) + : (None, 0) => (Some(StackType::ExternRef), 1), + TableSet(elem_index: |ops| ops.table_size + 1 => u32) + : (Some(StackType::ExternRef), 1) => (None, 0), + + GlobalGet(global_index: |ops| ops.num_globals => u32) + : (None, 0) => (Some(StackType::ExternRef), 1), + GlobalSet(global_index: |ops| ops.num_globals => u32) + : (Some(StackType::ExternRef), 1) => (None, 0), + + LocalGet(local_index: |ops| ops.num_params => u32) + : (None, 0) => (Some(StackType::ExternRef), 1), + LocalSet(local_index: |ops| ops.num_params => u32) + : (Some(StackType::ExternRef), 1) => (None, 0), + + // Handled specially in result_types() + StructNew(type_index: |ops| ops.max_types => u32) + : (None, 0) => (Some(StackType::Anything), 1), + TakeStructCall(type_index: |ops| ops.max_types => u32) + : (Some(StackType::Struct(None)), 1) => (None, 0), + // Handled specially in operand_types() + // StackType::Anythng is just a placeholder + TakeTypedStructCall(type_index: |ops| ops.max_types => u32) + : (Some(StackType::Anything), 1) => (None, 0), + + Drop : (Some(StackType::Anything), 1) => (None, 0), + + Null : (None, 0) => (Some(StackType::ExternRef), 1), } impl GcOp { @@ -497,16 +523,12 @@ impl GcOp { } Self::StructNew(x) => { func.instruction(&Instruction::StructNew(x + struct_type_base)); - func.instruction(&Instruction::Call(take_structref_idx)); } - Self::TakeStructCall(x) => { - func.instruction(&Instruction::StructNew(x + struct_type_base)); + Self::TakeStructCall(_x) => { func.instruction(&Instruction::Call(take_structref_idx)); } Self::TakeTypedStructCall(x) => { - let s = struct_type_base + x; let f = typed_first_func_index + x; - func.instruction(&Instruction::StructNew(s)); func.instruction(&Instruction::Call(f)); } } diff --git a/crates/fuzzing/src/generators/gc_ops/tests.rs b/crates/fuzzing/src/generators/gc_ops/tests.rs index b5907c0f84c6..62214874ed98 100644 --- a/crates/fuzzing/src/generators/gc_ops/tests.rs +++ b/crates/fuzzing/src/generators/gc_ops/tests.rs @@ -250,7 +250,7 @@ fn test_wat_string() -> mutatis::Result<()> { global.set 0 global.get 0 struct.new 5 - call 3 + drop drop drop drop diff --git a/crates/fuzzing/src/generators/gc_ops/types.rs b/crates/fuzzing/src/generators/gc_ops/types.rs index 5cd44ae61863..a000855142ec 100644 --- a/crates/fuzzing/src/generators/gc_ops/types.rs +++ b/crates/fuzzing/src/generators/gc_ops/types.rs @@ -1,6 +1,7 @@ //! Types for the `gc` operations. use crate::generators::gc_ops::limits::GcOpsLimits; +use crate::generators::gc_ops::ops::GcOp; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; @@ -88,3 +89,80 @@ impl Types { ); } } + +/// This is used to track the requirements for the operands of an operation. +#[derive(Copy, Clone, Debug)] +pub enum StackType { + /// Any value is used for reauested operand not a type left on stack (only for Drop and specially handled ops) + Anything, + /// `externref` + ExternRef, + /// None = any non-null `(ref $*)`; Some(t) = exact `(ref $t)` + Struct(Option), +} + +impl StackType { + /// Fixes the stack type to match the given requirement. + pub fn fixup_stack_type( + req: StackType, + stack: &mut Vec, + out: &mut Vec, + num_types: u32, + ) { + match req { + Self::Anything => { + // Anything can accept any type - just pop if available + // If stack is empty, synthesize null (anyref compatible) + if stack.pop().is_none() { + // Create a null externref (will be converted if needed) + Self::emit(GcOp::Null(), stack, out, num_types); + stack.pop(); // consume just-synthesized externref + } + } + Self::ExternRef => match stack.last() { + Some(Self::ExternRef) => { + stack.pop(); + } + _ => { + Self::emit(GcOp::Null(), stack, out, num_types); + stack.pop(); // consume just-synthesized externref + } + }, + Self::Struct(wanted) => { + let ok = match (wanted, stack.last()) { + (Some(wanted), Some(Self::Struct(Some(s)))) => *s == wanted, + (None, Some(Self::Struct(_))) => true, + _ => false, + }; + + if ok { + stack.pop(); + } else { + // Ensure there *is* a struct to consume. + let t = match wanted { + Some(t) => Self::clamp(t, num_types), + None => Self::clamp(0, num_types), + }; + Self::emit(GcOp::StructNew(t), stack, out, num_types); + stack.pop(); // consume the synthesized struct + } + } + } + } + + pub(crate) fn emit(op: GcOp, stack: &mut Vec, out: &mut Vec, num_types: u32) { + out.push(op); + op.result_types().iter().for_each(|ty| { + // Clamp struct type indices when pushing to stack + let clamped_ty = match ty { + Self::Struct(Some(t)) => Self::Struct(Some(Self::clamp(*t, num_types))), + other => *other, + }; + stack.push(clamped_ty); + }); + } + + fn clamp(t: u32, n: u32) -> u32 { + if n == 0 { 0 } else { t % n } + } +} From 8423dcc9dfac6f1e0c722e26b613ec0e4a5fe3ce Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Mon, 10 Nov 2025 18:15:56 -0700 Subject: [PATCH 2/9] Add new test for fixup() --- crates/fuzzing/src/generators/gc_ops/tests.rs | 80 +++++++++++++++++++ crates/fuzzing/src/generators/gc_ops/types.rs | 2 +- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/crates/fuzzing/src/generators/gc_ops/tests.rs b/crates/fuzzing/src/generators/gc_ops/tests.rs index 62214874ed98..4c4d062a5160 100644 --- a/crates/fuzzing/src/generators/gc_ops/tests.rs +++ b/crates/fuzzing/src/generators/gc_ops/tests.rs @@ -297,3 +297,83 @@ fn emits_empty_rec_groups_and_validates() -> mutatis::Result<()> { Ok(()) } + +#[test] +fn fixup_check_types_and_indexes() -> mutatis::Result<()> { + let _ = env_logger::try_init(); + + // Create GcOps with 5 types so that 7 % 5 = 2 + let mut ops = test_ops(5, 5, 5); + ops.limits.max_types = 5; + + // We create max types 5 and out ouf bounds for the type index + ops.ops = vec![ + GcOp::TakeTypedStructCall(27), + GcOp::GlobalSet(0), + GcOp::StructNew(24), + GcOp::LocalSet(0), + ]; + + // Call fixup to resolve dependencies + // this should fix the types by inserting missing types + // also put the indexes in the correct bounds + ops.fixup(); + + // Verify that fixup() + // The expected sequence should be: + // 1. StructNew(_) - inserted by fixup to satisfy TakeTypedStructCall(_) + // 2. TakeTypedStructCall(_) - now has Struct(_) on stack + // 3. Null() - inserted by fixup to satisfy GlobalSet(0) + // 4. GlobalSet(0) - now has ExternRef on stack + // 5. StructNew(_) - produces Struct(_) + // 6. Null() - inserted by fixup to satisfy LocalSet(0) + // 7. LocalSet(0) - now has ExternRef on stack + // 8. Drop() - inserted by fixup to consume ExternRef before Drop() + + // This is the expected sequence in wat format: + // loop ;; label = @1 + // struct.new 7 + // call 6 + // ref.null extern + // global.set 0 + // struct.new 9 + // ref.null extern + // local.set 0 + // drop + // br 0 (;@1;) + // end + + // Find the index of TakeTypedStructCall(_) after fixup + let take_call_idx = ops + .ops + .iter() + .position(|op| matches!(op, GcOp::TakeTypedStructCall(_))) + .expect("TakeTypedStructCall(_) should be present after fixup"); + + // Verify that StructNew(_) appears before TakeTypedStructCall(_) + let struct_new_2_before = ops + .ops + .iter() + .take(take_call_idx) + .any(|op| matches!(op, GcOp::StructNew(_))); + + assert!( + struct_new_2_before, + "fixup should insert StructNew(_) before TakeTypedStructCall(_) to satisfy the dependency" + ); + + // Verify the sequence validates correctly + let wasm = ops.to_wasm_binary(); + let wat = wasmprinter::print_bytes(&wasm).unwrap(); + log::debug!("wat:\n{}", wat); + let feats = wasmparser::WasmFeatures::default(); + feats.reference_types(); + feats.gc(); + let mut validator = wasmparser::Validator::new_with_features(feats); + assert!( + validator.validate_all(&wasm).is_ok(), + "GC validation should pass after fixup" + ); + + Ok(()) +} diff --git a/crates/fuzzing/src/generators/gc_ops/types.rs b/crates/fuzzing/src/generators/gc_ops/types.rs index a000855142ec..289da7c190a1 100644 --- a/crates/fuzzing/src/generators/gc_ops/types.rs +++ b/crates/fuzzing/src/generators/gc_ops/types.rs @@ -114,7 +114,7 @@ impl StackType { // Anything can accept any type - just pop if available // If stack is empty, synthesize null (anyref compatible) if stack.pop().is_none() { - // Create a null externref (will be converted if needed) + // Create a null externref Self::emit(GcOp::Null(), stack, out, num_types); stack.pop(); // consume just-synthesized externref } From 800198666ad7b42285fba044833b63bc25e7bc10 Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Mon, 10 Nov 2025 18:58:27 -0700 Subject: [PATCH 3/9] Address clippy failure --- crates/fuzzing/src/generators/gc_ops/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fuzzing/src/generators/gc_ops/tests.rs b/crates/fuzzing/src/generators/gc_ops/tests.rs index 4c4d062a5160..f1911de86593 100644 --- a/crates/fuzzing/src/generators/gc_ops/tests.rs +++ b/crates/fuzzing/src/generators/gc_ops/tests.rs @@ -365,7 +365,7 @@ fn fixup_check_types_and_indexes() -> mutatis::Result<()> { // Verify the sequence validates correctly let wasm = ops.to_wasm_binary(); let wat = wasmprinter::print_bytes(&wasm).unwrap(); - log::debug!("wat:\n{}", wat); + log::debug!("{wat}"); let feats = wasmparser::WasmFeatures::default(); feats.reference_types(); feats.gc(); From 0777c114473020d38f4afc7d246360c9c480f115 Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Thu, 15 Jan 2026 07:35:48 -0700 Subject: [PATCH 4/9] Update fixup and num_types --- crates/fuzzing/src/generators/gc_ops/ops.rs | 28 ++++++++++++++------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index 5531f16eef35..37ba4e38a61b 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -269,15 +269,15 @@ impl GcOps { continue; } - op.fixup(&self.limits); + op.fixup(&self.limits, num_types); - match &mut op { - GcOp::StructNew(t) | GcOp::TakeStructCall(t) | GcOp::TakeTypedStructCall(t) => { - if num_types > 0 { - *t = *t % num_types; - } - } - _ => {} + if num_types == 0 + && matches!( + op, + GcOp::StructNew(..) | GcOp::TakeStructCall(..) | GcOp::TakeTypedStructCall(..) + ) + { + continue; } op.operand_types().iter().for_each(|ty| { @@ -368,7 +368,7 @@ macro_rules! define_gc_ops { } } - pub(crate) fn fixup(&mut self, limits: &GcOpsLimits) { + pub(crate) fn fixup(&mut self, limits: &GcOpsLimits, num_types: u32) { match self { $( Self::$op( $( $( $limit_var ),* )? ) => { @@ -381,6 +381,16 @@ macro_rules! define_gc_ops { } )* } + match self { + Self::StructNew(t) + | Self::TakeStructCall(t) + | Self::TakeTypedStructCall(t) => { + if num_types > 0 { + *t %= num_types; + } + } + _ => {} + } } pub(crate) fn generate( From 315511677599c1740983d59c32cac2fc44c5ed48 Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Thu, 15 Jan 2026 08:37:40 -0700 Subject: [PATCH 5/9] Remove vector allocation each time --- crates/fuzzing/src/generators/gc_ops/ops.rs | 44 ++++++++++++------- crates/fuzzing/src/generators/gc_ops/types.rs | 22 +++++++--- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index 37ba4e38a61b..87412d857faf 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -280,12 +280,15 @@ impl GcOps { continue; } - op.operand_types().iter().for_each(|ty| { - StackType::fixup_stack_type(*ty, &mut stack, &mut new_ops, num_types); - }); + let mut operand_types = Vec::new(); + op.operand_types(&mut operand_types); + for ty in operand_types { + StackType::fixup_stack_type(ty, &mut stack, &mut new_ops, num_types); + } // Finally, emit the op itself (updates stack abstractly) - StackType::emit(op, &mut stack, &mut new_ops, num_types); + let mut result_types = Vec::new(); + StackType::emit(op, &mut stack, &mut new_ops, num_types, &mut result_types); } // Balance any leftovers with drops (works for any type) @@ -312,8 +315,9 @@ macro_rules! define_gc_ops { : ($operand_req:expr, $params:expr) => ($result_type:expr, $results:expr) , )* ) => { - #[derive(Copy, Clone, Debug, Serialize, Deserialize)] + /// The operations that can be performed by the `gc` function. + #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub enum GcOp { $( #[allow(missing_docs, reason = "macro-generated code")] @@ -337,14 +341,19 @@ macro_rules! define_gc_ops { } #[allow(unreachable_patterns, reason = "macro-generated code")] - pub(crate) fn operand_types(&self) -> Vec { + pub(crate) fn operand_types(&self, out: &mut Vec) { match self { // special-cases - Self::TakeTypedStructCall(t) => vec![StackType::Struct(Some(*t))], + Self::TakeTypedStructCall(t) => { + out.push(StackType::Struct(Some(*t))); + } $( - Self::$op(..) => match $operand_req { - None => vec![], - Some(req) => if $params == 0 { vec![] } else { vec![req; $params] }, + Self::$op(..) => { + if let Some(req) = $operand_req { + for _ in 0..$params { + out.push(req); + } + } } ),* } @@ -355,14 +364,19 @@ macro_rules! define_gc_ops { } #[allow(unreachable_patterns, reason = "macro-generated code")] - pub(crate) fn result_types(&self) -> Vec { + pub(crate) fn result_types(&self, out: &mut Vec) { match self { // special-cases - Self::StructNew(t) => vec![StackType::Struct(Some(*t))], + Self::StructNew(t) => { + out.push(StackType::Struct(Some(*t))); + } $( - Self::$op(..) => match $result_type { - None => vec![], - Some(ty) => if $results == 0 { vec![] } else { vec![ty; $results] }, + Self::$op(..) => { + if let Some(req) = $result_type { + for _ in 0..$results { + out.push(req); + } + } } ),* } diff --git a/crates/fuzzing/src/generators/gc_ops/types.rs b/crates/fuzzing/src/generators/gc_ops/types.rs index 289da7c190a1..c01129973c31 100644 --- a/crates/fuzzing/src/generators/gc_ops/types.rs +++ b/crates/fuzzing/src/generators/gc_ops/types.rs @@ -109,13 +109,14 @@ impl StackType { out: &mut Vec, num_types: u32, ) { + let mut result_types = Vec::new(); match req { Self::Anything => { // Anything can accept any type - just pop if available // If stack is empty, synthesize null (anyref compatible) if stack.pop().is_none() { // Create a null externref - Self::emit(GcOp::Null(), stack, out, num_types); + Self::emit(GcOp::Null(), stack, out, num_types, &mut result_types); stack.pop(); // consume just-synthesized externref } } @@ -124,7 +125,7 @@ impl StackType { stack.pop(); } _ => { - Self::emit(GcOp::Null(), stack, out, num_types); + Self::emit(GcOp::Null(), stack, out, num_types, &mut result_types); stack.pop(); // consume just-synthesized externref } }, @@ -143,23 +144,30 @@ impl StackType { Some(t) => Self::clamp(t, num_types), None => Self::clamp(0, num_types), }; - Self::emit(GcOp::StructNew(t), stack, out, num_types); + Self::emit(GcOp::StructNew(t), stack, out, num_types, &mut result_types); stack.pop(); // consume the synthesized struct } } } } - pub(crate) fn emit(op: GcOp, stack: &mut Vec, out: &mut Vec, num_types: u32) { + pub(crate) fn emit( + op: GcOp, + stack: &mut Vec, + out: &mut Vec, + num_types: u32, + result_types: &mut Vec, + ) { out.push(op); - op.result_types().iter().for_each(|ty| { - // Clamp struct type indices when pushing to stack + + op.result_types(result_types); + for ty in result_types { let clamped_ty = match ty { Self::Struct(Some(t)) => Self::Struct(Some(Self::clamp(*t, num_types))), other => *other, }; stack.push(clamped_ty); - }); + } } fn clamp(t: u32, n: u32) -> u32 { From 2897ea132175c943eeb9608e03309d25b07d4b12 Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Thu, 15 Jan 2026 09:27:25 -0700 Subject: [PATCH 6/9] Address GcOps::generate --- .../fuzzing/src/generators/gc_ops/mutator.rs | 73 ++----------------- crates/fuzzing/src/generators/gc_ops/ops.rs | 1 + crates/fuzzing/src/generators/gc_ops/tests.rs | 1 + 3 files changed, 8 insertions(+), 67 deletions(-) diff --git a/crates/fuzzing/src/generators/gc_ops/mutator.rs b/crates/fuzzing/src/generators/gc_ops/mutator.rs index 581408d38356..05ece4912a21 100644 --- a/crates/fuzzing/src/generators/gc_ops/mutator.rs +++ b/crates/fuzzing/src/generators/gc_ops/mutator.rs @@ -1,15 +1,6 @@ //! Mutators for the `gc` operations. -use crate::generators::gc_ops::{ - limits::{ - GcOpsLimits, MAX_OPS, MAX_REC_GROUPS_RANGE, MAX_TYPES_RANGE, NUM_GLOBALS_RANGE, - NUM_PARAMS_RANGE, TABLE_SIZE_RANGE, - }, - ops::{GcOp, GcOps}, - types::{RecGroupId, TypeId, Types}, -}; - -use mutatis::mutators as m; +use crate::generators::gc_ops::ops::{GcOp, GcOps}; use mutatis::{Candidates, Context, DefaultMutate, Generate, Mutate, Result as MutResult}; /// A mutator for the gc ops @@ -63,64 +54,12 @@ impl<'a> arbitrary::Arbitrary<'a> for GcOps { } impl Generate for GcOpsMutator { - fn generate(&mut self, ctx: &mut Context) -> MutResult { - let num_params = m::range(NUM_PARAMS_RANGE).generate(ctx)?; - let num_globals = m::range(NUM_GLOBALS_RANGE).generate(ctx)?; - let table_size = m::range(TABLE_SIZE_RANGE).generate(ctx)?; - - let max_rec_groups = m::range(MAX_REC_GROUPS_RANGE).generate(ctx)?; - let max_types = m::range(MAX_TYPES_RANGE).generate(ctx)?; - - let mut ops = GcOps { - limits: GcOpsLimits { - num_params, - num_globals, - table_size, - max_rec_groups, - max_types, - }, - ops: { - let mut v = vec![GcOp::Null(), GcOp::Drop(), GcOp::Gc()]; - if num_params > 0 { - v.push(GcOp::LocalSet(0)); - v.push(GcOp::LocalGet(0)); - } - if num_globals > 0 { - v.push(GcOp::GlobalSet(0)); - v.push(GcOp::GlobalGet(0)); - } - if max_types > 0 { - v.push(GcOp::StructNew(0)); - } - v - }, - types: Types::new(), - }; - - for i in 0..ops.limits.max_rec_groups { - ops.types.insert_rec_group(RecGroupId(i)); - } - - if ops.limits.max_rec_groups > 0 { - for i in 0..ops.limits.max_types { - let tid = TypeId(i); - let gid = RecGroupId(m::range(0..=ops.limits.max_rec_groups - 1).generate(ctx)?); - - ops.types.insert_empty_struct(tid, gid); - } - } - - let mut stack: usize = 0; - - while ops.ops.len() < MAX_OPS { - let (op, new_stack_len) = GcOp::generate(ctx, &ops, stack)?; - ops.ops.push(op); - stack = new_stack_len; - } + fn generate(&mut self, _ctx: &mut Context) -> MutResult { + let mut ops = GcOps::default(); + let mut session = mutatis::Session::new(); - // Drop any leftover refs on the stack. - for _ in 0..stack { - ops.ops.push(GcOp::Drop()); + for _ in 0..64 { + session.mutate(&mut ops)?; } Ok(ops) diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index 87412d857faf..4f0f3ab44616 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -407,6 +407,7 @@ macro_rules! define_gc_ops { } } + // TODO: This is stack-depth-biased generation. It needs to be updated. pub(crate) fn generate( ctx: &mut mutatis::Context, ops: &GcOps, diff --git a/crates/fuzzing/src/generators/gc_ops/tests.rs b/crates/fuzzing/src/generators/gc_ops/tests.rs index f1911de86593..3c6ff4941a41 100644 --- a/crates/fuzzing/src/generators/gc_ops/tests.rs +++ b/crates/fuzzing/src/generators/gc_ops/tests.rs @@ -83,6 +83,7 @@ fn mutate_gc_ops_with_default_mutator() -> mutatis::Result<()> { let wat = wasmprinter::print_bytes(&wasm).expect("[-] Failed .print_bytes(&wasm)."); let result = validator.validate_all(&wasm); log::debug!("{wat}"); + println!("{wat}"); assert!( result.is_ok(), "\n[-] Invalid wat: {}\n\t\t==== Failed Wat ====\n{}", From a152936a6711e6d4dc0b9475c18accf80b602e4f Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Thu, 15 Jan 2026 09:28:15 -0700 Subject: [PATCH 7/9] Remove println from tests --- crates/fuzzing/src/generators/gc_ops/tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/fuzzing/src/generators/gc_ops/tests.rs b/crates/fuzzing/src/generators/gc_ops/tests.rs index 3c6ff4941a41..f1911de86593 100644 --- a/crates/fuzzing/src/generators/gc_ops/tests.rs +++ b/crates/fuzzing/src/generators/gc_ops/tests.rs @@ -83,7 +83,6 @@ fn mutate_gc_ops_with_default_mutator() -> mutatis::Result<()> { let wat = wasmprinter::print_bytes(&wasm).expect("[-] Failed .print_bytes(&wasm)."); let result = validator.validate_all(&wasm); log::debug!("{wat}"); - println!("{wat}"); assert!( result.is_ok(), "\n[-] Invalid wat: {}\n\t\t==== Failed Wat ====\n{}", From 0fca03ff628c36ef14c9db3cec8eb7baedc5d89f Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Sun, 1 Feb 2026 19:40:56 -0700 Subject: [PATCH 8/9] Address the Anything stack type --- .../fuzzing/src/generators/gc_ops/mutator.rs | 3 +- crates/fuzzing/src/generators/gc_ops/ops.rs | 72 ++++++------------- crates/fuzzing/src/generators/gc_ops/types.rs | 23 +++--- 3 files changed, 31 insertions(+), 67 deletions(-) diff --git a/crates/fuzzing/src/generators/gc_ops/mutator.rs b/crates/fuzzing/src/generators/gc_ops/mutator.rs index 05ece4912a21..9ed63b003e39 100644 --- a/crates/fuzzing/src/generators/gc_ops/mutator.rs +++ b/crates/fuzzing/src/generators/gc_ops/mutator.rs @@ -12,8 +12,7 @@ impl Mutate for GcOpsMutator { if !c.shrink() { c.mutation(|ctx| { if let Some(idx) = ctx.rng().gen_index(ops.ops.len() + 1) { - let stack = ops.abstract_stack_depth(idx); - let (op, _new_stack_size) = GcOp::generate(ctx, &ops, stack)?; + let op = GcOp::generate(ctx, &ops)?; ops.ops.insert(idx, op); } Ok(()) diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index 4f0f3ab44616..140e25eeda62 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -218,19 +218,6 @@ impl GcOps { module.finish() } - /// Computes the abstract stack depth after executing all operations - pub fn abstract_stack_depth(&self, index: usize) -> usize { - debug_assert!(index <= self.ops.len()); - let mut stack: usize = 0; - for op in self.ops.iter().take(index) { - let pop = op.operands_len(); - let push = op.results_len(); - stack = stack.saturating_sub(pop); - stack += push; - } - stack - } - /// Fixes this test case such that it becomes valid. /// /// This is necessary because a random mutation (e.g. removing an op in the @@ -283,7 +270,7 @@ impl GcOps { let mut operand_types = Vec::new(); op.operand_types(&mut operand_types); for ty in operand_types { - StackType::fixup_stack_type(ty, &mut stack, &mut new_ops, num_types); + StackType::fixup(ty, &mut stack, &mut new_ops, num_types); } // Finally, emit the op itself (updates stack abstractly) @@ -336,10 +323,6 @@ macro_rules! define_gc_ops { match self { $( Self::$op(..) => stringify!($op), )* } } - pub(crate) fn operands_len(&self) -> usize { - match self { $( Self::$op(..) => $params, )* } - } - #[allow(unreachable_patterns, reason = "macro-generated code")] pub(crate) fn operand_types(&self, out: &mut Vec) { match self { @@ -359,10 +342,6 @@ macro_rules! define_gc_ops { } } - pub(crate) fn results_len(&self) -> usize { - match self { $( Self::$op(..) => $results, )* } - } - #[allow(unreachable_patterns, reason = "macro-generated code")] pub(crate) fn result_types(&self, out: &mut Vec) { match self { @@ -407,32 +386,24 @@ macro_rules! define_gc_ops { } } - // TODO: This is stack-depth-biased generation. It needs to be updated. + /// Generate an arbitrary op without stack-depth awareness. + /// The fixup pass will make the sequence valid. pub(crate) fn generate( ctx: &mut mutatis::Context, ops: &GcOps, - stack: usize, - ) -> mutatis::Result<(GcOp, usize)> { + ) -> mutatis::Result { let mut valid_choices: Vec< - fn(&mut Context, &GcOpsLimits, usize) -> mutatis::Result<(GcOp, usize)> + fn(&mut Context, &GcOpsLimits) -> mutatis::Result > = vec![]; $( - #[allow(unused_comparisons, reason = "macro-generated code")] - if stack >= $params $($( - && { - let limit_fn = $limit as fn(&GcOpsLimits) -> $ty; - (limit_fn)(&ops.limits) > 0 - } - )*)? { - valid_choices.push($op); - } + valid_choices.push($op); )* let f = *ctx.rng() .choose(&valid_choices) .expect("should always have a valid op choice"); - (f)(ctx, &ops.limits, stack) + (f)(ctx, &ops.limits) } } @@ -441,20 +412,20 @@ macro_rules! define_gc_ops { fn $op( _ctx: &mut mutatis::Context, _limits: &GcOpsLimits, - stack: usize, - ) -> mutatis::Result<(GcOp, usize)> { - #[allow(unused_comparisons, reason = "macro-generated code")] - { debug_assert!(stack >= $params); } - + ) -> mutatis::Result { let op = GcOp::$op( $($({ let limit_fn = $limit as fn(&GcOpsLimits) -> $ty; let limit = (limit_fn)(_limits); - debug_assert!(limit > 0); - m::range(0..=limit - 1).generate(_ctx)? + // Generate a value even if limit is 0; fixup will handle it + if limit > 0 { + m::range(0..=limit - 1).generate(_ctx)? + } else { + 0 + } }),*)? ); - Ok((op, stack - $params + $results)) + Ok(op) } )* }; @@ -482,17 +453,18 @@ define_gc_ops! { LocalSet(local_index: |ops| ops.num_params => u32) : (Some(StackType::ExternRef), 1) => (None, 0), - // Handled specially in result_types() StructNew(type_index: |ops| ops.max_types => u32) - : (None, 0) => (Some(StackType::Anything), 1), + : (None, 0) => (None, 0), + TakeStructCall(type_index: |ops| ops.max_types => u32) : (Some(StackType::Struct(None)), 1) => (None, 0), - // Handled specially in operand_types() - // StackType::Anythng is just a placeholder + TakeTypedStructCall(type_index: |ops| ops.max_types => u32) - : (Some(StackType::Anything), 1) => (None, 0), + : (None, 0) => (None, 0), + + Drop : (Some(StackType::Any), 1) => (None, 0), + - Drop : (Some(StackType::Anything), 1) => (None, 0), Null : (None, 0) => (Some(StackType::ExternRef), 1), } diff --git a/crates/fuzzing/src/generators/gc_ops/types.rs b/crates/fuzzing/src/generators/gc_ops/types.rs index c01129973c31..5cca2d0bfc9b 100644 --- a/crates/fuzzing/src/generators/gc_ops/types.rs +++ b/crates/fuzzing/src/generators/gc_ops/types.rs @@ -93,8 +93,9 @@ impl Types { /// This is used to track the requirements for the operands of an operation. #[derive(Copy, Clone, Debug)] pub enum StackType { - /// Any value is used for reauested operand not a type left on stack (only for Drop and specially handled ops) - Anything, + /// Any value is used for reauested operand not a + ///type left on stack (only for Drop and specially handled ops) + Any, /// `externref` ExternRef, /// None = any non-null `(ref $*)`; Some(t) = exact `(ref $t)` @@ -103,22 +104,14 @@ pub enum StackType { impl StackType { /// Fixes the stack type to match the given requirement. - pub fn fixup_stack_type( - req: StackType, - stack: &mut Vec, - out: &mut Vec, - num_types: u32, - ) { + pub fn fixup(req: StackType, stack: &mut Vec, out: &mut Vec, num_types: u32) { let mut result_types = Vec::new(); match req { - Self::Anything => { - // Anything can accept any type - just pop if available - // If stack is empty, synthesize null (anyref compatible) - if stack.pop().is_none() { - // Create a null externref + Self::Any => { + if stack.is_empty() { Self::emit(GcOp::Null(), stack, out, num_types, &mut result_types); - stack.pop(); // consume just-synthesized externref } + stack.pop(); // always consume exactly one value } Self::ExternRef => match stack.last() { Some(Self::ExternRef) => { @@ -159,7 +152,7 @@ impl StackType { result_types: &mut Vec, ) { out.push(op); - + result_types.clear(); op.result_types(result_types); for ty in result_types { let clamped_ty = match ty { From 1307a3c56f72f76be80b1b82e0387283679b505a Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Mon, 2 Feb 2026 19:36:30 -0700 Subject: [PATCH 9/9] Address the second round of reviews --- crates/fuzzing/src/generators/gc_ops/ops.rs | 88 ++++++++----------- crates/fuzzing/src/generators/gc_ops/types.rs | 18 ++-- 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index 140e25eeda62..ef8cfa5a25ea 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -235,7 +235,10 @@ impl GcOps { let mut new_ops = Vec::with_capacity(self.ops.len()); let mut stack: Vec = Vec::new(); - let num_types = self.types.type_defs.len() as u32; + let num_types = u32::try_from(self.types.type_defs.len()) + .expect("types len should be within u32 range"); + + let mut operand_types = Vec::new(); for mut op in self.ops.iter().copied() { if self.limits.max_types == 0 @@ -258,19 +261,16 @@ impl GcOps { op.fixup(&self.limits, num_types); - if num_types == 0 - && matches!( - op, - GcOp::StructNew(..) | GcOp::TakeStructCall(..) | GcOp::TakeTypedStructCall(..) - ) - { + let op = if let Some(op) = op.fixup(&self.limits, num_types) { + op + } else { continue; - } + }; - let mut operand_types = Vec::new(); + operand_types.clear(); op.operand_types(&mut operand_types); - for ty in operand_types { - StackType::fixup(ty, &mut stack, &mut new_ops, num_types); + for ty in &operand_types { + StackType::fixup(*ty, &mut stack, &mut new_ops, num_types); } // Finally, emit the op itself (updates stack abstractly) @@ -299,7 +299,7 @@ macro_rules! define_gc_ops { $( $op:ident $( ( $($limit_var:ident : $limit:expr => $ty:ty),* ) )? - : ($operand_req:expr, $params:expr) => ($result_type:expr, $results:expr) , + : [ $($operand:expr),* $(,)? ] => [ $($result:expr),* $(,)? ] , )* ) => { @@ -324,19 +324,15 @@ macro_rules! define_gc_ops { } #[allow(unreachable_patterns, reason = "macro-generated code")] - pub(crate) fn operand_types(&self, out: &mut Vec) { + pub(crate) fn operand_types(&self, out: &mut Vec>) { + match self { - // special-cases Self::TakeTypedStructCall(t) => { - out.push(StackType::Struct(Some(*t))); + out.push(Some(StackType::Struct(Some(*t)))); } $( Self::$op(..) => { - if let Some(req) = $operand_req { - for _ in 0..$params { - out.push(req); - } - } + $( out.push($operand); )* } ),* } @@ -345,23 +341,14 @@ macro_rules! define_gc_ops { #[allow(unreachable_patterns, reason = "macro-generated code")] pub(crate) fn result_types(&self, out: &mut Vec) { match self { - // special-cases Self::StructNew(t) => { out.push(StackType::Struct(Some(*t))); } - $( - Self::$op(..) => { - if let Some(req) = $result_type { - for _ in 0..$results { - out.push(req); - } - } - } - ),* + $( Self::$op(..) => { $( out.push($result); )* }, )* } } - pub(crate) fn fixup(&mut self, limits: &GcOpsLimits, num_types: u32) { + pub(crate) fn fixup(&mut self, limits: &GcOpsLimits, num_types: u32) -> Option { match self { $( Self::$op( $( $( $limit_var ),* )? ) => { @@ -378,12 +365,15 @@ macro_rules! define_gc_ops { Self::StructNew(t) | Self::TakeStructCall(t) | Self::TakeTypedStructCall(t) => { - if num_types > 0 { - *t %= num_types; + if num_types == 0 { + return None; } + *t %= num_types; } _ => {} } + + Some(*self) } /// Generate an arbitrary op without stack-depth awareness. @@ -432,41 +422,41 @@ macro_rules! define_gc_ops { } define_gc_ops! { - Gc : (None, 0) => (Some(StackType::ExternRef), 3), + Gc : [] => [StackType::ExternRef, StackType::ExternRef, StackType::ExternRef], - MakeRefs : (None, 0) => (Some(StackType::ExternRef), 3), - TakeRefs : (Some(StackType::ExternRef), 3) => (None, 0), + MakeRefs : [] => [StackType::ExternRef, StackType::ExternRef, StackType::ExternRef], + TakeRefs : [Some(StackType::ExternRef), Some(StackType::ExternRef), Some(StackType::ExternRef)] => [], // Add one to make sure that out of bounds table accesses are possible, but still rare. TableGet(elem_index: |ops| ops.table_size + 1 => u32) - : (None, 0) => (Some(StackType::ExternRef), 1), + : [] => [StackType::ExternRef], TableSet(elem_index: |ops| ops.table_size + 1 => u32) - : (Some(StackType::ExternRef), 1) => (None, 0), + : [Some(StackType::ExternRef)] => [], GlobalGet(global_index: |ops| ops.num_globals => u32) - : (None, 0) => (Some(StackType::ExternRef), 1), + : [] => [StackType::ExternRef], GlobalSet(global_index: |ops| ops.num_globals => u32) - : (Some(StackType::ExternRef), 1) => (None, 0), + : [Some(StackType::ExternRef)] => [], LocalGet(local_index: |ops| ops.num_params => u32) - : (None, 0) => (Some(StackType::ExternRef), 1), + : [] => [StackType::ExternRef], LocalSet(local_index: |ops| ops.num_params => u32) - : (Some(StackType::ExternRef), 1) => (None, 0), + : [Some(StackType::ExternRef)] => [], + // `StructNew` result is special-cased to push `Struct(Some(t))`, so results list is empty. StructNew(type_index: |ops| ops.max_types => u32) - : (None, 0) => (None, 0), + : [] => [], TakeStructCall(type_index: |ops| ops.max_types => u32) - : (Some(StackType::Struct(None)), 1) => (None, 0), + : [Some(StackType::Struct(None))] => [], + // `TakeTypedStructCall` operand is special-cased to require `Struct(Some(t))`, so operands list is empty. TakeTypedStructCall(type_index: |ops| ops.max_types => u32) - : (None, 0) => (None, 0), - - Drop : (Some(StackType::Any), 1) => (None, 0), - + : [] => [], + Drop : [None] => [], - Null : (None, 0) => (Some(StackType::ExternRef), 1), + Null : [] => [StackType ::ExternRef], } impl GcOp { diff --git a/crates/fuzzing/src/generators/gc_ops/types.rs b/crates/fuzzing/src/generators/gc_ops/types.rs index 5cca2d0bfc9b..fb51491950f4 100644 --- a/crates/fuzzing/src/generators/gc_ops/types.rs +++ b/crates/fuzzing/src/generators/gc_ops/types.rs @@ -93,27 +93,29 @@ impl Types { /// This is used to track the requirements for the operands of an operation. #[derive(Copy, Clone, Debug)] pub enum StackType { - /// Any value is used for reauested operand not a - ///type left on stack (only for Drop and specially handled ops) - Any, /// `externref` ExternRef, - /// None = any non-null `(ref $*)`; Some(t) = exact `(ref $t)` + /// `(ref $*)`; Struct(Option), } impl StackType { /// Fixes the stack type to match the given requirement. - pub fn fixup(req: StackType, stack: &mut Vec, out: &mut Vec, num_types: u32) { + pub fn fixup( + req: Option, + stack: &mut Vec, + out: &mut Vec, + num_types: u32, + ) { let mut result_types = Vec::new(); match req { - Self::Any => { + None => { if stack.is_empty() { Self::emit(GcOp::Null(), stack, out, num_types, &mut result_types); } stack.pop(); // always consume exactly one value } - Self::ExternRef => match stack.last() { + Some(Self::ExternRef) => match stack.last() { Some(Self::ExternRef) => { stack.pop(); } @@ -122,7 +124,7 @@ impl StackType { stack.pop(); // consume just-synthesized externref } }, - Self::Struct(wanted) => { + Some(Self::Struct(wanted)) => { let ok = match (wanted, stack.last()) { (Some(wanted), Some(Self::Struct(Some(s)))) => *s == wanted, (None, Some(Self::Struct(_))) => true,