diff --git a/crates/fuzzing/src/generators/gc_ops/limits.rs b/crates/fuzzing/src/generators/gc_ops/limits.rs index 1b667a46a608..b4830321c11b 100644 --- a/crates/fuzzing/src/generators/gc_ops/limits.rs +++ b/crates/fuzzing/src/generators/gc_ops/limits.rs @@ -17,6 +17,8 @@ pub const MAX_REC_GROUPS_RANGE: RangeInclusive = 0..=10; pub const MAX_FIELDS_RANGE: RangeInclusive = 0..=8; /// Range for the length of created arrays. pub const ARRAY_LENGTH_RANGE: RangeInclusive = 1..=16; +/// Maximum number of fields that can be inlined in a struct type. +pub const MAX_INLINE_CONSTRUCTION: u32 = 8; /// Limits controlling the structure of a generated Wasm module. #[derive(Clone, Debug, Serialize, Deserialize, mutatis::Mutate)] diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index 8f791d9b6684..d64fcf32e53f 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -2,8 +2,8 @@ use crate::generators::gc_ops::types::StackType; use crate::generators::gc_ops::{ - limits::GcOpsLimits, - types::{CompositeType, RecGroupId, StructField, TypeId, Types}, + limits::{GcOpsLimits, MAX_INLINE_CONSTRUCTION}, + types::{CompositeType, EmitCtx, RecGroupId, StructField, TypeId, Types, emit_new}, }; use mutatis::Generate; use serde::{Deserialize, Serialize}; @@ -39,6 +39,22 @@ fn array_element<'a>( }) } +/// The fields of the struct type at `type_index`, or `None` if that index is +/// out of range or names an array type. +fn struct_fields<'a>( + types: &'a Types, + encoding_order: &[TypeId], + type_index: u32, +) -> Option<&'a [StructField]> { + encoding_order + .get(usize::try_from(type_index).unwrap()) + .and_then(|tid| types.type_defs.get(tid)) + .and_then(|def| match &def.composite_type { + CompositeType::Struct(st) => Some(st.fields.as_slice()), + CompositeType::Array(_) => None, + }) +} + /// The base offsets and indices for various Wasm entities within /// their index spaces in the the encoded Wasm binary. #[derive(Clone, Copy)] @@ -499,7 +515,10 @@ impl GcOps { let typed_local_base: u32 = array_local_idx + 1; let typed_local2_base: u32 = typed_local_base + concrete_count; - for _ in 0..2 { + // A third set holds one shared prototype per concrete type; see + // `Types::prototype_types`. + let proto_local_base: u32 = typed_local2_base + concrete_count; + for _ in 0..3 { for i in 0..concrete_count { let concrete = struct_type_base + i; local_decls.push(( @@ -534,16 +553,48 @@ impl GcOps { array_length: self.limits.array_length, }; + let mut inhabitable = BTreeMap::new(); + self.types.inhabitable(&mut inhabitable); + let struct_ref_target = self.types.least_rank_inhabitable_struct(&inhabitable); + + // Map each prototyped type to the local holding its instance. A type + // absent from this map is cheap enough to rebuild at every use. + let proto_order = self + .types + .prototype_types(&inhabitable, MAX_INLINE_CONSTRUCTION); + let protos: BTreeMap = proto_order + .iter() + .map(|tid| { + let dense = type_ids_to_index[tid] - struct_type_base; + (*tid, proto_local_base + dense) + }) + .collect(); + + let ctx = EmitCtx { + types: &self.types, + struct_ref_target, + protos: &protos, + type_ids_to_index: &type_ids_to_index, + }; + let mut func = Function::new(local_decls); func.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty)); + + // Refill the prototypes at the top of every iteration, so the ops below + // never read a null local and each iteration allocates a fresh set of + // objects for the collector to find and reclaim. `proto_order` is in + // rank order, so a prototype's referents are already built. + for tid in &proto_order { + emit_new(*tid, &mut func, ctx); + func.instruction(&Instruction::LocalSet(protos[tid])); + } for op in &self.ops { op.encode( &mut func, scratch_local, storage_bases, - &self.types, + ctx, &encoding_order, - &type_ids_to_index, ); } func.instruction(&Instruction::Br(0)); @@ -1363,10 +1414,10 @@ impl GcOp { func: &mut Function, scratch_local: u32, encoding_bases: WasmEncodingBases, - types: &Types, + ctx: EmitCtx<'_>, encoding_order: &[TypeId], - type_ids_to_index: &BTreeMap, ) { + let types = ctx.types; let gc_func_idx = 0; let take_refs_func_idx = 1; let make_refs_func_idx = 2; @@ -1459,37 +1510,57 @@ impl GcOp { ))); } Self::StructNew { type_index: x } => { - if let Some(tid) = encoding_order.get(usize::try_from(x).unwrap()) { - if let Some(CompositeType::Struct(st)) = - types.type_defs.get(tid).map(|def| &def.composite_type) - { - for field in &st.fields { - field.field_type.emit_default_const(func, type_ids_to_index); - } - } + for field in struct_fields(types, encoding_order, x).unwrap_or(&[]) { + field.field_type.emit_default_const(func, ctx); } func.instruction(&Instruction::StructNew(encoding_bases.struct_type_base + x)); } Self::StructNewDefault { type_index: x } => { - func.instruction(&Instruction::StructNewDefault( - encoding_bases.struct_type_base + x, - )); + // `struct.new_default` requires every field to be defaultable, + // which a non-nullable reference is not. Build those fields + // explicitly instead; the resulting value and stack effect are + // the same. + let fields = struct_fields(types, encoding_order, x).unwrap_or(&[]); + if fields.iter().all(|f| f.field_type.is_defaultable()) { + func.instruction(&Instruction::StructNewDefault( + encoding_bases.struct_type_base + x, + )); + } else { + for field in fields { + field.field_type.emit_default_const(func, ctx); + } + func.instruction(&Instruction::StructNew(encoding_bases.struct_type_base + x)); + } } Self::ArrayNewDefault { type_index: x } => { // Create a default-initialized array of a fixed length so most // subsequent indexed accesses are in-bounds. - func.instruction(&Instruction::I32Const( - encoding_bases.array_length.cast_signed(), - )); - func.instruction(&Instruction::ArrayNewDefault( - encoding_bases.struct_type_base + x, - )); + match array_element(types, encoding_order, x) { + // As above, `array.new_default` needs a defaultable + // element. `array.new` takes the initial value explicitly, + // so it covers non-nullable elements at the same length. + Some(element) if !element.field_type.is_defaultable() => { + element.field_type.emit_default_const(func, ctx); + func.instruction(&Instruction::I32Const( + encoding_bases.array_length.cast_signed(), + )); + func.instruction(&Instruction::ArrayNew( + encoding_bases.struct_type_base + x, + )); + } + _ => { + func.instruction(&Instruction::I32Const( + encoding_bases.array_length.cast_signed(), + )); + func.instruction(&Instruction::ArrayNewDefault( + encoding_bases.struct_type_base + x, + )); + } + } } Self::ArrayNew { type_index: x } => { if let Some(element) = array_element(types, encoding_order, x) { - element - .field_type - .emit_default_const(func, type_ids_to_index); + element.field_type.emit_default_const(func, ctx); } func.instruction(&Instruction::I32Const( encoding_bases.array_length.cast_signed(), @@ -1499,9 +1570,7 @@ impl GcOp { Self::ArrayNewFixed { type_index: x, n } => { if let Some(element) = array_element(types, encoding_order, x) { for _ in 0..n { - element - .field_type - .emit_default_const(func, type_ids_to_index); + element.field_type.emit_default_const(func, ctx); } } func.instruction(&Instruction::ArrayNewFixed { @@ -1749,9 +1818,7 @@ impl GcOp { func.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); func.instruction(&Instruction::Else); func.instruction(&Instruction::LocalGet(typed_local)); - fields[idx] - .field_type - .emit_default_const(func, type_ids_to_index); + fields[idx].field_type.emit_default_const(func, ctx); let idx = u32::try_from(idx).unwrap(); func.instruction(&Instruction::StructSet { struct_type_index: wasm_type, @@ -1942,9 +2009,7 @@ impl GcOp { func.instruction(&Instruction::Else); func.instruction(&Instruction::LocalGet(typed_local)); func.instruction(&Instruction::I32Const(index.cast_signed())); - element - .field_type - .emit_default_const(func, type_ids_to_index); + element.field_type.emit_default_const(func, ctx); func.instruction(&Instruction::ArraySet(wasm_type)); func.instruction(&Instruction::End); } @@ -1969,9 +2034,7 @@ impl GcOp { func.instruction(&Instruction::Else); func.instruction(&Instruction::LocalGet(typed_local)); func.instruction(&Instruction::I32Const(offset.cast_signed())); - element - .field_type - .emit_default_const(func, type_ids_to_index); + element.field_type.emit_default_const(func, ctx); func.instruction(&Instruction::I32Const(len.cast_signed())); func.instruction(&Instruction::ArrayFill(wasm_type)); func.instruction(&Instruction::End); diff --git a/crates/fuzzing/src/generators/gc_ops/types.rs b/crates/fuzzing/src/generators/gc_ops/types.rs index e85e818c5cd2..f9704ecab646 100644 --- a/crates/fuzzing/src/generators/gc_ops/types.rs +++ b/crates/fuzzing/src/generators/gc_ops/types.rs @@ -96,10 +96,6 @@ macro_rules! define_field_type_enum { /// Concrete `(ref null? $t)` referencing a defined struct type. Ref { - // `nullable` is ignored because support for non-nullable - // references isn't implemented yet and so `Types::fixup` - // unconditionally forces it to `true`. Mutating it here would - // be a waste of time. #[mutatis(ignore)] nullable: bool, @@ -151,15 +147,28 @@ macro_rules! define_field_type_enum { matches!(self, FieldType::I8 | FieldType::I16) } - /// Emit a default constant for this field type onto the stack - pub fn emit_default_const( + /// Returns `true` if this field type can be default-constructed. + pub fn is_defaultable(self) -> bool { + !matches!( + self, + FieldType::StructRef { nullable: false } + | FieldType::Ref { nullable:false, .. } + ) + } + + /// Emit a value of this field type onto the stack. + /// + /// Nullable references use `ref.null`, which is always available. + /// A non-nullable reference has no null to fall back on, so we must + /// construct a real object; see `emit_new`. + pub(crate) fn emit_default_const( self, func: &mut wasm_encoder::Function, - type_ids_to_index: &BTreeMap, + ctx: EmitCtx<'_>, ) { match self { $( FieldType::$variant => { func.instruction(&$default_val); } )* - FieldType::StructRef { .. } => { + FieldType::StructRef { nullable: true } => { func.instruction(&wasm_encoder::Instruction::RefNull( wasm_encoder::HeapType::Abstract { shared: false, @@ -167,21 +176,22 @@ macro_rules! define_field_type_enum { }, )); } - FieldType::Ref { type_id, .. } => { - // See `to_storage_type`: a missing index is a fixup bug. - // Emitting `ref.null struct` here would produce a value - // that does not match the field's concrete `(ref null - // $t)` type and yield an invalid module, so panic. - let &idx = type_ids_to_index.get(&type_id).unwrap_or_else(|| { - unreachable!( - "concrete struct reference to {type_id:?} missing from \ - index map; fixup should keep all reference targets" - ) - }); + FieldType::StructRef { nullable: false } => { + // `(ref struct)` is satisfied by any struct, so build + // the cheapest one. `fix_uninhabitable` only leaves this + // field non-nullable when such a struct exists. + let tid = ctx.struct_ref_target.expect("non-nullable struct ref must have a target"); + emit_ref_to(tid, func, ctx); + } + FieldType::Ref { nullable: true, type_id } => { + let &idx = ctx.type_ids_to_index.get(&type_id).expect("concrete struct reference to {type_id:?} missing"); func.instruction(&wasm_encoder::Instruction::RefNull( wasm_encoder::HeapType::Concrete(idx), )); } + FieldType::Ref { nullable: false, type_id } => { + emit_ref_to(type_id, func, ctx); + } } } } @@ -189,6 +199,58 @@ macro_rules! define_field_type_enum { } for_each_field_type!(define_field_type_enum); +/// Everything the construction emitters need. +#[derive(Clone, Copy)] +pub(crate) struct EmitCtx<'a> { + /// The type graph being encoded. + pub(crate) types: &'a Types, + /// The struct to build for a non-nullable `(ref struct)` field. + pub(crate) struct_ref_target: Option, + /// Types with a shared prototype, mapped to the local holding it. + pub(crate) protos: &'a BTreeMap, + /// The Wasm type index assigned to each `TypeId`. + pub(crate) type_ids_to_index: &'a BTreeMap, +} + +/// Emit a reference to the given type, constructing a new instance if necessary. +fn emit_ref_to(type_id: TypeId, func: &mut wasm_encoder::Function, ctx: EmitCtx<'_>) { + match ctx.protos.get(&type_id) { + Some(&proto_idx) => { + func.instruction(&wasm_encoder::Instruction::LocalGet(proto_idx)); + func.instruction(&wasm_encoder::Instruction::RefAsNonNull); + } + None => emit_new(type_id, func, ctx), + } +} + +/// Emit a new instance of the given type, constructing its fields recursively. +pub(crate) fn emit_new(type_id: TypeId, func: &mut wasm_encoder::Function, ctx: EmitCtx<'_>) { + let &idx = ctx + .type_ids_to_index + .get(&type_id) + .unwrap_or_else(|| unreachable!("reference to {type_id:?} missing from index map")); + let def = ctx + .types + .type_defs + .get(&type_id) + .unwrap_or_else(|| unreachable!("reference to {type_id:?}, which has no definition")); + + match &def.composite_type { + CompositeType::Struct(st) => { + for field in &st.fields { + field.field_type.emit_default_const(func, ctx); + } + func.instruction(&wasm_encoder::Instruction::StructNew(idx)); + } + CompositeType::Array(_) => { + func.instruction(&wasm_encoder::Instruction::ArrayNewFixed { + array_type_index: idx, + array_size: 0, + }); + } + } +} + /// A single field within a struct type. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, mutatis::Mutate)] pub struct StructField { @@ -819,24 +881,19 @@ impl Types { let valid_type_ids: BTreeSet = self.type_defs.keys().copied().collect(); for def in self.type_defs.values_mut() { for field in def.composite_type.fields_mut() { - match &mut field.field_type { - FieldType::StructRef { nullable } => *nullable = true, - FieldType::Ref { nullable, type_id } => { - if !valid_type_ids.contains(type_id) { - if let Some(live) = - nearest_live_type_id(&valid_type_ids, *type_id, None) - { - *type_id = live; - } - } - if valid_type_ids.contains(type_id) { - *nullable = true; - } else { - // There are no types at all to point at. - field.field_type = FieldType::StructRef { nullable: true }; + // Nullability is left alone here; step 11 relaxes only the + // non-nullable references that cannot be satisfied. + if let FieldType::Ref { type_id, .. } = &mut field.field_type { + if !valid_type_ids.contains(type_id) { + if let Some(live) = nearest_live_type_id(&valid_type_ids, *type_id, None) { + *type_id = live; } } - _ => {} + if !valid_type_ids.contains(type_id) { + // There are no types at all to point at, so there is + // nothing this reference could ever be made to hold. + field.field_type = FieldType::StructRef { nullable: true }; + } } } } @@ -897,12 +954,211 @@ impl Types { } } + // 11. Relax non-nullable reference fields that cannot be satisfied. + self.fix_uninhabitable(); + debug_assert!(self.is_well_formed(limits)); - // 11. Compute encoding order (reuses type_to_group from step 9). + // 12. Compute encoding order (reuses type_to_group from step 9). self.encoding_order_grouped(encoding_order_grouped, &type_to_group); } + /// Whether `field` can be given a value using only the types in `ok`. + fn field_satisfiable(&self, field: FieldType, ok: &BTreeMap) -> bool { + match field { + FieldType::Ref { + nullable: false, + type_id, + } => ok.contains_key(&type_id), + FieldType::StructRef { nullable: false } => ok + .keys() + .any(|&c| !self.type_defs[&c].composite_type.is_array()), + _ => true, + } + } + + /// Compute the types that can be constructed + pub(crate) fn inhabitable(&self, out: &mut BTreeMap) { + out.clear(); + let mut round = 0; + loop { + // `out` does not change during a sweep, so this is loop-invariant. + let has_struct = out + .keys() + .any(|&c| !self.type_defs[&c].composite_type.is_array()); + + let mut newly = Vec::new(); + for (&tid, def) in &self.type_defs { + if out.contains_key(&tid) { + continue; + } + + let constructible = + def.composite_type + .fields() + .iter() + .all(|field| match field.field_type { + FieldType::StructRef { nullable: false } => has_struct, + other => self.field_satisfiable(other, out), + }); + + if constructible { + newly.push(tid); + } + } + + if newly.is_empty() { + return; + } + for tid in newly { + out.insert(tid, round); + } + round += 1; + } + } + + /// Return the least-ranked inhabitable struct type, if any. + pub(crate) fn least_rank_inhabitable_struct( + &self, + inhabitable: &BTreeMap, + ) -> Option { + inhabitable + .iter() + .filter(|(tid, _)| !self.type_defs[tid].composite_type.is_array()) + .min_by_key(|&(_, &rank)| rank) + .map(|(&tid, _)| tid) + } + + /// Relax fields of a struct type to be nullable when uninhabitable, so that it can be constructed. + pub(crate) fn fix_uninhabitable(&mut self) { + let mut ok = BTreeMap::new(); + loop { + self.inhabitable(&mut ok); + if ok.len() == self.type_defs.len() { + return; + } + + let bad = *self + .type_defs + .keys() + .find(|tid| !ok.contains_key(tid)) + .expect("inhabitable is a strict subset here, so an offender exists"); + + // Relax only the fields that cannot be satisfied, so well-founded + // non-nullable references on the same type survive. + let fields: Vec = self.type_defs[&bad] + .composite_type + .fields() + .iter() + .map(|f| f.field_type) + .collect(); + for (index, field) in fields.into_iter().enumerate() { + if !self.field_satisfiable(field, &ok) { + self.relax_field(bad, index); + } + } + } + } + + /// Relax a field of a struct type to be nullable, and propagate the change to all subtypes. + fn relax_field(&mut self, tid: TypeId, index: usize) { + // Walk up to the highest ancestor that still has this field. Supertype + // cycles are already broken by step 9, so this terminates. + let mut root = tid; + while let Some(sup) = self.type_defs[&root].supertype { + if self.type_defs[&sup].composite_type.fields().len() <= index { + break; + } + root = sup; + } + + // Relax it there and in every descendant that inherits it. + let affected: Vec = self + .type_defs + .keys() + .copied() + .filter(|&t| self.is_subtype(t, root)) + .collect(); + for t in affected { + let fields = self + .type_defs + .get_mut(&t) + .unwrap() + .composite_type + .fields_mut(); + let Some(field) = fields.get_mut(index) else { + continue; + }; + match &mut field.field_type { + FieldType::Ref { nullable, .. } | FieldType::StructRef { nullable } => { + *nullable = true; + } + _ => {} + } + } + } + + /// Compute the set of types that should be encoded as prototypes, i.e. those + /// that are referenced by other types. + pub(crate) fn prototype_types( + &self, + inhabitable: &BTreeMap, + threshold: u32, + ) -> Vec { + let mut by_rank: Vec<(u32, TypeId)> = inhabitable.iter().map(|(&t, &r)| (r, t)).collect(); + by_rank.sort(); + + let abstract_target = self.least_rank_inhabitable_struct(inhabitable); + + let mut referenced: BTreeSet = BTreeSet::new(); + for def in self.type_defs.values() { + for field in def.composite_type.fields() { + match field.field_type { + FieldType::Ref { + nullable: false, + type_id, + } => { + referenced.insert(type_id); + } + FieldType::StructRef { nullable: false } => { + referenced.extend(abstract_target); + } + _ => {} + } + } + } + + let mut cost: BTreeMap = BTreeMap::new(); + let mut protos = Vec::new(); + for (_, tid) in by_rank { + let inline = match &self.type_defs[&tid].composite_type { + CompositeType::Array(_) => 1, + CompositeType::Struct(st) => st.fields.iter().fold(1u32, |acc: u32, field| { + let field_cost = match field.field_type { + FieldType::Ref { + nullable: false, + type_id, + } => cost.get(&type_id).copied().unwrap_or(1), + FieldType::StructRef { nullable: false } => abstract_target + .and_then(|t| cost.get(&t).copied()) + .unwrap_or(1), + _ => 1, + }; + acc.saturating_add(field_cost) + }), + }; + if inline > threshold { + if referenced.contains(&tid) { + protos.push(tid); + } + cost.insert(tid, 2); + } else { + cost.insert(tid, inline); + } + } + protos + } + /// Check if the types are well-formed and within configured limits, i.e. /// rec/type counts are within limits, /// every type belongs to exactly one rec group, @@ -947,21 +1203,13 @@ impl Types { return false; } - // Reference fields must be nullable (non-nullable references are - // deferred), and concrete references must target an existing type. + // Concrete references must target an existing type. for field in fields { - match field.field_type { - FieldType::StructRef { nullable } | FieldType::Ref { nullable, .. } - if !nullable => - { - log::debug!("[-] Failed: type {tid:?} has a non-nullable reference field"); - return false; - } - FieldType::Ref { type_id, .. } if !self.type_defs.contains_key(&type_id) => { + if let FieldType::Ref { type_id, .. } = field.field_type { + if !self.type_defs.contains_key(&type_id) { log::debug!("[-] Failed: type {tid:?} references missing type {type_id:?}"); return false; } - _ => {} } } @@ -1007,6 +1255,21 @@ impl Types { } } } + + // Every type must be constructible. A non-nullable reference field has + // no default value, so a cycle of them leaves types that validate but + // can never be instantiated. See `fix_uninhabitable`. + let mut inhabitable = BTreeMap::new(); + self.inhabitable(&mut inhabitable); + if inhabitable.len() != self.type_defs.len() { + log::debug!( + "[-] Failed: {} of {} types are uninhabitable", + self.type_defs.len() - inhabitable.len(), + self.type_defs.len() + ); + return false; + } + true } }