diff --git a/crates/formality-core/src/binder/fuzz.rs b/crates/formality-core/src/binder/fuzz.rs index 6f17c723..bdffb2d8 100644 --- a/crates/formality-core/src/binder/fuzz.rs +++ b/crates/formality-core/src/binder/fuzz.rs @@ -1,6 +1,4 @@ -use bolero::{TypeGenerator, ValueGenerator}; - -use crate::{fold::CoreFold, fuzz::PushGuard, language::Language, variable::CoreBoundVar}; +use crate::{fold::CoreFold, fuzz::Fuzzable, language::Language, variable::CoreBoundVar}; use super::{fresh_bound_var, CoreBinder}; @@ -9,16 +7,24 @@ pub struct KindVec { pub vec: Vec, } -impl bolero::TypeGenerator for KindVec +impl Fuzzable for KindVec where - L::Kind: TypeGenerator, + L::Kind: Fuzzable, { - fn generate(driver: &mut D) -> Option { - let num_kinds: usize = L::fuzz_binder_range().get().generate(driver)?; - Some(KindVec { - vec: (0..num_kinds) - .map(|_| driver.gen()) - .collect::>()?, + fn estimate_cardinality(cx: &mut crate::fuzz::FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| { + guard.binder_range().len() as f64 * guard.estimate_cardinality::() + }) + } + + fn fuzz(cx: &mut crate::fuzz::FuzzCx) -> Option { + cx.enter_fuzz::(|guard| { + let binder_range = guard.binder_range().clone(); + let len = guard.fuzz_usize(binder_range)?; + let vec = (0..len) + .map(|_| guard.fuzz::()) + .collect::>()?; + Some(KindVec { vec }) }) } } @@ -66,16 +72,22 @@ impl PushKindGuard { } } -impl bolero::TypeGenerator for CoreBinder +impl Fuzzable for CoreBinder where - T: bolero::TypeGenerator + CoreFold, - L::Kind: bolero::TypeGenerator, + T: Fuzzable + CoreFold, { - /// Generate a binder with some fresh data inside. - fn generate(driver: &mut D) -> Option { - let kinds: KindVec = driver.gen()?; - let guard = L::open_fuzz_binder(&kinds.vec); - let bound_term: T = driver.gen()?; - Some(guard.into_binder(bound_term)) + fn estimate_cardinality(cx: &mut crate::fuzz::FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| { + guard.estimate_cardinality::>() * guard.estimate_cardinality::() + }) + } + + fn fuzz(cx: &mut crate::fuzz::FuzzCx) -> Option { + cx.enter_fuzz::(|guard| { + let kinds = guard.fuzz::>()?; + let guard = L::open_fuzz_binder(&kinds.vec); + let bound_term = guard.fuzz::()?; + Some(guard.into_binder(bound_term)) + }) } } diff --git a/crates/formality-core/src/fuzz.rs b/crates/formality-core/src/fuzz.rs index fcb35419..5c6c857d 100644 --- a/crates/formality-core/src/fuzz.rs +++ b/crates/formality-core/src/fuzz.rs @@ -2,134 +2,557 @@ //! //! [f]: https://rust-lang.github.io/a-mir-formality/formality_core/fuzzing.html -use std::fmt::Debug; -use std::sync::OnceLock; -use std::{ops::Deref, sync::RwLock}; +use std::any::{Any, TypeId}; +use std::marker::PhantomData; +use std::ops::Deref; +use std::sync::Arc; -use crate::Map; +use crate::{Map, Set}; -/// A global singleton accessible from anywhere. -/// Used to thread state for fuzzing. -/// -/// See the Formality Book [chapter on fuzzing][f] for more details. -/// -/// [f]: https://rust-lang.github.io/a-mir-formality/formality_core/fuzzing.html -pub struct FuzzSingleton { - data: OnceLock>, +/// Underlying fuzz driver used to drive the rest of fuzzing +pub trait FuzzDriver { + fn fuzz_u32(&mut self, range: std::ops::Range) -> Option; + fn fuzz_usize(&mut self, range: std::ops::Range) -> Option; + fn fuzz_f64(&mut self, range: std::ops::Range) -> Option; } -/// Trait for clearing the value of a singleton. -/// Not invoked from outside. -trait FuzzClear { - fn clear(&self); -} +/// Fuzzing context +pub struct FuzzCx<'d> { + /// Maximum recursion depth for any given type + max_depth: u32, + + /// Valid range for u32/usize integer literals + literal_range: std::ops::Range, + + /// Range of elements to create for a generic collection (vector, map, etc) + collection_range: std::ops::Range, + + /// Range of elements to create for a generic collection (vector, map, etc) + binder_range: std::ops::Range, + + /// Stores a range of possible values for the type with the given type-id. + /// The `Any` will always be a `Vec`. + valid_values: Map>, -/// Guard that clears the singleton back to its default value after `set` is called. -pub struct SetGuard<'s> { - pool: &'s dyn FuzzClear, + /// Stores a map for the given type-ids. + /// The `Any` will always be a `Map`. + key_values: Map<(TypeId, TypeId), Box>, + + /// Current recursion depth for any given type. + depth: Map, + + /// Cached weights for a given type. + weights: Map<(TypeId, u32), f64>, + + /// Underlying bolero driver. + driver: Box, } -impl FuzzSingleton -where - I: Debug + Default + Eq, -{ - /// Create a new instance; this is a `const` so that the value can be stored in a `static`. - pub const fn new() -> Self { +impl<'d> FuzzCx<'d> { + /// Create a new fuzzing context with the given driver and max-depth. + /// All other context (e.g., key-values) will be empty. + pub fn new(driver: impl FuzzDriver + 'd) -> Self { Self { - data: OnceLock::new(), + max_depth: 3, + collection_range: 0..3, + literal_range: 0..22, + binder_range: 0..2, + valid_values: Map::default(), + key_values: Map::default(), + depth: Map::default(), + weights: Map::default(), + driver: Box::new(driver), } } - /// Internal method: access the data, initializing if needed. - fn data(&self) -> &RwLock { - self.data.get_or_init(|| RwLock::new(I::default())) + /// Configure maximum depth. + pub fn set_max_depth(&mut self, max_depth: u32) { + self.max_depth = max_depth; } - /// Set the set of available names. - /// - /// This is intended to be called once per fuzzing session, - /// so it will panic if the value has already been "set" from its default value. - /// - /// Returns a guard that will restore the value to the default when dropped. - /// Once this guard is dropped, you can call `set` again. - pub fn set(&self, new_data: I) -> SetGuard<'_> { - let mut data = self.data().write().unwrap(); - let old_data = std::mem::replace(&mut *data, new_data); + /// Configure range of collection length. + pub fn set_collection_range(&mut self, collection_range: std::ops::Range) { + self.collection_range = collection_range; + } + + /// Range of collection length + pub fn collection_range(&self) -> &std::ops::Range { + &self.collection_range + } + + /// Range for integer literals + pub fn literal_range(&self) -> &std::ops::Range { + &self.literal_range + } + + /// Configure number of variables to consider for binders. + pub fn set_binder_range(&mut self, binder_range: std::ops::Range) { + self.binder_range = binder_range; + } + + /// Number of variables to consider for binders. + pub fn binder_range(&self) -> &std::ops::Range { + &self.binder_range + } + + /// Returns a reference to the value of the given type `T`. + /// If the type has not been set, then `None` is returned. + pub fn get(&self) -> Option<&T> { + let id = TypeId::of::(); + let value = self.valid_values.get(&id)?; + value.downcast_ref::() + } + + /// Lookup the value associated with the given value of type `K`. + /// If no map has been set for this (K,V) type, or if the key is not + /// found in the map that was set, returns `None`. + #[track_caller] + pub fn key_value(&self, key: &K) -> Option { + let key_id = TypeId::of::(); + let value_id = TypeId::of::(); + let Some(map) = self.key_values.get(&(key_id, value_id)) else { + panic!( + "no key/value map set for (`{} -> {}`)", + std::any::type_name::(), + std::any::type_name::(), + ) + }; + let map = map.downcast_ref::>().unwrap(); + map.get(key).cloned() + } + + /// Lookup the value associated with the given value of type `K`. + /// If no map has been set for this (K,V) type, or if the key is not + /// found in the map that was set, returns `None`. + pub fn set_key_values(&mut self, map: Map) { + let key_id = TypeId::of::(); + let value_id = TypeId::of::(); + let old_value = self + .key_values + .insert((key_id, value_id), Box::new(map) as Box); + assert!( - I::default() == old_data, - "cannot fuzz more than one program at a time, already fuzzing `{old_data:?}`" + old_value.is_none(), + "already set the key-values for `{} -> {}`", + std::any::type_name::(), + std::any::type_name::(), ); - SetGuard { pool: self } } - /// Access the data. Will deadlock if you try to set before the guard is dropped. - pub fn get(&self) -> impl Deref + '_ { - self.data().read().unwrap() + /// Set the values for the given type `T`. + /// When fuzzing, the impl can invoke [`Guard::pick_value`][] + /// to select from these values. + pub fn set_values(&mut self, values: Vec) { + let id = TypeId::of::(); + let old_value = self + .valid_values + .insert(id, Box::new(values) as Box); + assert!( + old_value.is_none(), + "already set the values for `{}`", + std::any::type_name::() + ); + } + + /// Push new available values for `T`. + /// Returns a guard that will pop them off again. + /// Guard must be dropped in stack order (i.e., this guard must be dropped before the guards from any prior pushes). + /// + /// Used to bring bound variables into scope. + pub fn push_values(&mut self, new_values: Vec) -> PushGuard<'_, 'd, T> { + let id = TypeId::of::(); + + let mut value = self + .valid_values + .remove(&id) + .map(|v| v.downcast::>().unwrap()) + .unwrap_or_default(); + + let old_len = value.len(); + value.extend(new_values); + let new_len = value.len(); + + self.valid_values + .insert(id, Box::new(value) as Box); + + PushGuard { + cx: self, + old_len, + new_len, + phantom: PhantomData, + } + } + + /// Undoes the effect of a previous call to `push_values` + fn pop_values(&mut self, old_len: usize, new_len: usize) { + let id = TypeId::of::(); + + let mut value = self + .valid_values + .remove(&id) + .map(|v| v.downcast::>().unwrap()) + .unwrap(); + + assert_eq!(value.len(), new_len, "stack discipline violated"); + value.truncate(old_len); + + self.valid_values + .insert(id, Box::new(value) as Box); + } + + /// Attempts to enter the given type `T` at the given depth. + /// Returns `None` if the recursion depth is exceeded. + /// Otherwise, returns a `Some(g)` with a guard that should be dropped when you have finished. + fn enter( + &mut self, + op: impl FnOnce(&mut EnterGuard<'_, 'd, T>) -> R, + ) -> Option { + let id = TypeId::of::(); + + let depth = self.depth.entry(id).or_insert(0); + if *depth >= self.max_depth { + return None; + } + + *depth += 1; + let depth = *depth; + + let result = op(&mut EnterGuard { + cx: self, + depth, + phantom: PhantomData::, + }); + + if depth == 1 { + self.depth.remove(&id); + } else { + *self.depth.get_mut(&id).unwrap() = depth - 1; + } + + Some(result) + } + + /// Begin computing the "estimate cardinarily" result + /// for the type `T`. The returned value will be cached. + /// Tracks depth and returns 0 if depth would be exceeded. + pub fn enter_estimate_cardinality( + &mut self, + op: impl FnOnce(&mut EnterGuard<'_, 'd, T>) -> f64, + ) -> f64 { + self.enter::(|guard| { + let id = TypeId::of::(); + let depth = guard.depth; + if let Some(&v) = guard.cx.weights.get(&(id, depth)) { + return v; + } + + let result = op(guard); + guard.cx.weights.insert((id, depth), result); + result + }) + .unwrap_or(0_f64) + } + + /// Attempts to enter the given type `T` at the given depth. + /// Returns `None` if the recursion depth is exceeded. + /// Otherwise, returns a `Some(g)` with a guard that should be dropped when you have finished. + pub fn enter_fuzz( + &mut self, + op: impl FnOnce(&mut EnterGuard<'_, 'd, T>) -> Option, + ) -> Option { + self.enter(op).unwrap_or(None) } } -impl FuzzClear for FuzzSingleton +pub struct PushGuard<'cx, 'd, T: 'static> { + cx: &'cx mut FuzzCx<'d>, + old_len: usize, + new_len: usize, + phantom: PhantomData, +} + +impl<'cx, 'd, T: 'static> Drop for PushGuard<'cx, 'd, T> { + fn drop(&mut self) { + self.cx.pop_values::(self.old_len, self.new_len); + } +} + +fn expected_value(r: &std::ops::Range) -> f64 { + let diff = r.end - r.start; + (r.start as f64) + (diff as f64) / 2.0 +} + +/// Guard value given when you invoke one of the `enter_` routines for a given type. +/// Gives access to some of the metadata around that type. +pub struct EnterGuard<'a, 'd, T: 'static> { + cx: &'a mut FuzzCx<'d>, + depth: u32, + phantom: PhantomData, +} + +impl<'d, T: 'static> Deref for EnterGuard<'_, 'd, T> { + type Target = FuzzCx<'d>; + + fn deref(&self) -> &Self::Target { + self.cx + } +} + +impl<'d, T: 'static> EnterGuard<'_, 'd, T> { + pub fn depth(&self) -> u32 { + self.depth + } + + /// Number of available values set in the fuzz context for this type. + /// Used for identifier types and other things that pick from a known universe. + pub fn num_available_values(&mut self) -> usize { + let id = TypeId::of::(); + let Some(values) = self.cx.valid_values.get(&id) else { + return 0; + }; + let values = values.downcast_ref::>().unwrap(); + values.len() + } + + /// Pick one of the available values set in the fuzz context for this type. + /// Used for identifier types and other things that pick from a known universe. + #[track_caller] + pub fn pick_value(&mut self) -> Option + where + T: Clone, + { + let id = TypeId::of::(); + let Some(values) = self.cx.valid_values.get(&id) else { + panic!("no value values set for `{}`)", std::any::type_name::(),) + }; + let values = values.downcast_ref::>().unwrap(); + if values.is_empty() { + return None; + } + let i = self.cx.driver.fuzz_usize(0..values.len())?; + Some(values[i].clone()) + } + + /// Generate a fuzzed value of type `F`. + pub fn estimate_cardinality(&mut self) -> f64 { + F::estimate_cardinality(self.cx) + } + + /// Generate a fuzzed value of type `F`. + pub fn fuzz(&mut self) -> Option { + F::fuzz(self.cx) + } + + pub fn fuzz_sum_type( + &mut self, + options: &[(f64, &dyn Fn(&mut EnterGuard<'_, '_, T>) -> Option)], + fallback_option: usize, + ) -> Option { + let total: f64 = options.iter().map(|(w, _)| w).sum(); + let mut choice = self.cx.driver.fuzz_f64(0.0..total)?; + for (w, f) in options { + choice -= w; + if choice <= 0.0 { + return f(self); + } + } + options.get(fallback_option)?.1(self) + } + + pub fn fuzz_u32(&mut self, range: std::ops::Range) -> Option { + self.cx.driver.fuzz_u32(range) + } + + pub fn fuzz_usize(&mut self, range: std::ops::Range) -> Option { + self.cx.driver.fuzz_usize(range) + } + + pub fn fuzz_f64(&mut self, range: std::ops::Range) -> Option { + self.cx.driver.fuzz_f64(range) + } +} + +pub trait Fuzzable: Sized + 'static { + fn estimate_cardinality(cx: &mut FuzzCx) -> f64; + + fn fuzz(cx: &mut FuzzCx) -> Option; +} + +impl Fuzzable for Vec where - I: Debug + Default + Eq, + T: Fuzzable, { - /// Clear the set of available names. - /// Invoked by the guard returned from `set`. - fn clear(&self) { - *self.data().write().unwrap() = Default::default(); + fn estimate_cardinality(cx: &mut FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| { + guard.cx.collection_range.len() as f64 * guard.estimate_cardinality::() + }) + } + + fn fuzz(cx: &mut FuzzCx) -> Option { + let len = cx.driver.fuzz_usize(cx.collection_range.clone())?; + (0..len).map(|_| T::fuzz(cx)).collect() } } -impl FuzzSingleton> +impl Fuzzable for Set where - K: Debug + Ord, - V: Debug + Clone, + T: Fuzzable + Ord, { - pub fn get_key(&self, key: K) -> V { - self.get().get(&key).cloned().unwrap() + fn estimate_cardinality(cx: &mut FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| { + guard.cx.collection_range.len() as f64 * guard.estimate_cardinality::() + }) + } + + fn fuzz(cx: &mut FuzzCx) -> Option { + let len = cx.driver.fuzz_usize(cx.collection_range.clone())?; + (0..len).map(|_| T::fuzz(cx)).collect() } } -impl FuzzSingleton> { - /// Pick one of the available names from the fuzzer, - /// returning `None` if there are no available names or the fuzzer - /// ran out of data. - pub fn fuzz_pick(&self, driver: &mut impl bolero::Driver) -> Option { - let data = self.get(); - if data.is_empty() { - return None; +impl Fuzzable for Map +where + K: Fuzzable + Ord, + V: Fuzzable, +{ + fn estimate_cardinality(cx: &mut FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| { + guard.cx.collection_range.len() as f64 + * guard.estimate_cardinality::() + * guard.estimate_cardinality::() + }) + } + + fn fuzz(cx: &mut FuzzCx) -> Option { + let len = cx.driver.fuzz_usize(cx.collection_range.clone())?; + (0..len) + .map(|_| Some((K::fuzz(cx)?, V::fuzz(cx)?))) + .collect() + } +} + +impl Fuzzable for () { + fn estimate_cardinality(_cx: &mut FuzzCx) -> f64 { + 1.0 + } + + fn fuzz(_cx: &mut FuzzCx) -> Option { + Some(()) + } +} + +macro_rules! tuple_impl { + ($($A:ident),*) => { + impl<$($A,)*> Fuzzable for ($($A,)*) + where + $($A: Fuzzable,)* + { + fn estimate_cardinality(cx: &mut FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| -> f64 { + [ + $(guard.estimate_cardinality::<$A>(),)* + ].into_iter().sum() + }) + } + + fn fuzz(cx: &mut FuzzCx) -> Option { + cx.enter_fuzz::(|guard| { + Some(($(guard.fuzz::<$A>()?,)*)) + }) + } + } - let i = driver.gen_variant(data.len(), 0)?; - Some(data[i].clone()) + } +} + +tuple_impl!(A); +tuple_impl!(A, B); +tuple_impl!(A, B, C); +tuple_impl!(A, B, C, D); +tuple_impl!(A, B, C, D, E); +tuple_impl!(A, B, C, D, E, F); +tuple_impl!(A, B, C, D, E, F, G); +tuple_impl!(A, B, C, D, E, F, G, H); +tuple_impl!(A, B, C, D, E, F, G, H, I); +tuple_impl!(A, B, C, D, E, F, G, H, I, J); +tuple_impl!(A, B, C, D, E, F, G, H, I, J, K); +tuple_impl!(A, B, C, D, E, F, G, H, I, J, K, L); +tuple_impl!(A, B, C, D, E, F, G, H, I, J, K, L, M); + +impl Fuzzable for Arc +where + T: Fuzzable, +{ + fn estimate_cardinality(cx: &mut FuzzCx) -> f64 { + T::estimate_cardinality(cx) } - /// Push names into the pool of available values that will be used later - /// by `fuzz_pick`. Returns a guard that will pop them from the pool. - pub fn fuzz_push(&self, values: Vec) -> PushGuard<'_, E> { - let mut data = self.data().write().unwrap(); - let len = data.len(); - data.extend(values); - PushGuard { pool: self, len } + fn fuzz(cx: &mut FuzzCx) -> Option { + Some(Arc::new(T::fuzz(cx)?)) } } -/// Guard to pop names added by `fuzz_push`. -pub struct PushGuard<'s, E: Clone + Debug + Eq> { - pool: &'s FuzzSingleton>, - len: usize, +impl Fuzzable for Box +where + T: Fuzzable, +{ + fn estimate_cardinality(cx: &mut FuzzCx) -> f64 { + T::estimate_cardinality(cx) + } + + fn fuzz(cx: &mut FuzzCx) -> Option { + Some(Box::new(T::fuzz(cx)?)) + } } -impl Drop for PushGuard<'_, E> +impl Fuzzable for Option where - E: Clone + Debug + Eq, + T: Fuzzable, { - fn drop(&mut self) { - let mut data = self.pool.data().write().unwrap(); - data.truncate(self.len); + fn estimate_cardinality(cx: &mut FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| 1.0 + guard.estimate_cardinality::()) + } + + fn fuzz(cx: &mut FuzzCx) -> Option { + cx.enter_fuzz::(|guard| { + let options: &[(f64, &dyn Fn(&mut EnterGuard<'_, '_, Self>) -> Option)] = &[ + (1.0, &|_guard| None), + (guard.estimate_cardinality::(), &|guard| { + Some(Some(guard.fuzz::()?)) + }), + ]; + guard.fuzz_sum_type(options, 0) + }) } } -impl Drop for SetGuard<'_> { - fn drop(&mut self) { - self.pool.clear(); +impl Fuzzable for usize { + fn estimate_cardinality(cx: &mut FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| guard.cx.literal_range().len() as f64) + } + + fn fuzz(cx: &mut FuzzCx) -> Option { + cx.enter_fuzz::(|guard| { + let range = guard.cx.literal_range().clone(); + let i = guard + .cx + .driver + .fuzz_usize(range.start as usize..range.end as usize)?; + Some(i) + }) + } +} + +impl Fuzzable for u32 { + fn estimate_cardinality(cx: &mut FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| guard.cx.literal_range().len() as f64) + } + + fn fuzz(cx: &mut FuzzCx) -> Option { + cx.enter_fuzz::(|guard| { + let range = guard.cx.literal_range().clone(); + let i = guard.cx.driver.fuzz_u32(range)?; + Some(i) + }) } } diff --git a/crates/formality-core/src/lib.rs b/crates/formality-core/src/lib.rs index 0963d126..a93a8271 100644 --- a/crates/formality-core/src/lib.rs +++ b/crates/formality-core/src/lib.rs @@ -243,9 +243,15 @@ macro_rules! id { } } - impl $crate::bolero::TypeGenerator for $n { - fn generate(driver: &mut D) -> Option { - Self::fuzz_pool().fuzz_pick(driver) + impl $crate::fuzz::Fuzzable for $n { + fn estimate_cardinality(cx: &mut $crate::fuzz::FuzzCx<'_>) -> f64 { + cx.enter_estimate_cardinality::(|guard| { + guard.num_available_values() as f64 + }) + } + + fn fuzz(cx: &mut $crate::fuzz::FuzzCx<'_>) -> Option { + cx.enter_fuzz::(|guard| guard.pick_value()) } } diff --git a/crates/formality-core/src/term.rs b/crates/formality-core/src/term.rs index 664adf40..35a14d14 100644 --- a/crates/formality-core/src/term.rs +++ b/crates/formality-core/src/term.rs @@ -5,6 +5,7 @@ use crate::{ cast::{DowncastFrom, Upcast}, collections::Set, fold::CoreFold, + fuzz::Fuzzable, language::Language, parse::CoreParse, }; @@ -17,6 +18,7 @@ pub trait CoreTerm: + Eq + Hash + Debug + + Fuzzable + Upcast + DowncastFrom + 'static diff --git a/crates/formality-macros/src/fuzz.rs b/crates/formality-macros/src/fuzz.rs new file mode 100644 index 00000000..8c2e68ff --- /dev/null +++ b/crates/formality-macros/src/fuzz.rs @@ -0,0 +1,80 @@ +extern crate proc_macro; + +use proc_macro2::{Span, TokenStream}; +use quote::quote; + +pub(crate) fn derive_fuzz(mut s: synstructure::Structure) -> TokenStream { + s.underscore_const(true); + s.bind_with(|_| synstructure::BindStyle::Move); + + let cx = syn::Ident::new("cx", Span::call_site()); + let guard = syn::Ident::new("guard", Span::call_site()); + + // The cardinarlity of a variant is the product of its fields. + let estimate_cardinality_variants: Vec<_> = s + .variants() + .iter() + .map(|v| { + let estimate_cardinality_fields: Vec<_> = v + .ast() + .fields + .iter() + .map(|f| &f.ty) + .map(|ty| quote!(#guard.estimate_cardinality::<#ty>())) + .collect(); + quote!(1.0 #(* #estimate_cardinality_fields)*) + }) + .collect(); + + // The cardinarily of a type is equal to the sum of the cardinalities of its variants. + let estimate_cardinality_body = quote!(0.0 $( + $estimate_cardinality_variants)*); + + // Fuzzing a value involves first selecting a variant. + let fuzz_variants: Vec<_> = s + .variants() + .iter() + .zip(&estimate_cardinality_variants) + .map(|(v, cardinality)| { + let fuzz_call = v.construct(|field, _| { + let ty = &field.ty; + quote!(#guard.fuzz::<#ty>()?) + }); + + quote!((#cardinality, |#guard| #fuzz_call)) + }) + .collect(); + + // Pick a variant that has zero fields to use as fallback (if any). + let fewest_fields = s + .variants() + .iter() + .map(|v| v.ast().fields.len()) + .min() + .unwrap_or(0); + let fallback_variant: usize = s + .variants() + .iter() + .position(|v| v.ast().fields.len() == fewest_fields) + .unwrap_or(0); + + let fuzz_body = quote!( + #guard.fuzz_sum_type( + &[#(#fuzz_variants),*], + #fallback_variant, + ) + ); + + s.gen_impl(quote! { + use formality_core::fuzz::{FuzzCx, Fuzzable}; + + gen impl Fuzzable for @Self { + fn estimate_cardinality(#cx: &mut FuzzCx<'_>) -> f64 { + #cx.enter_estimate_cardinality::(|#guard| #estimate_cardinality_body) + } + + fn fuzz(#cx: &mut FuzzCx<'_>) -> Option { + #cx.enter_fuzz::(|#guard| #fuzz_body) + } + } + }) +} diff --git a/crates/formality-macros/src/lib.rs b/crates/formality-macros/src/lib.rs index 8eb00bd6..d481ece7 100644 --- a/crates/formality-macros/src/lib.rs +++ b/crates/formality-macros/src/lib.rs @@ -13,6 +13,7 @@ mod custom; mod debug; mod fixed_point; mod fold; +mod fuzz; mod parse; mod precedence; mod spec; @@ -41,6 +42,8 @@ pub fn term(args: TokenStream, input: TokenStream) -> TokenStream { synstructure::decl_derive!([Visit] => visit::derive_visit); +synstructure::decl_derive!([Fuzz] => fuzz::derive_fuzz); + #[proc_macro_attribute] pub fn fixed_point(args: TokenStream, input: TokenStream) -> TokenStream { let args = syn::parse_macro_input!(args as fixed_point::FixedPointArgs); diff --git a/crates/formality-macros/src/term.rs b/crates/formality-macros/src/term.rs index 372fdbc1..68940142 100644 --- a/crates/formality-macros/src/term.rs +++ b/crates/formality-macros/src/term.rs @@ -3,14 +3,7 @@ use quote::quote; use syn::DeriveInput; use crate::{ - attrs::{self, remove_formality_attributes}, - cast::{downcast_impls, upcast_impls}, - constructors::constructor_methods, - debug::derive_debug_with_spec, - fold::derive_fold, - parse::derive_parse_with_spec, - spec::FormalitySpec, - visit::derive_visit, + attrs::{self, remove_formality_attributes}, cast::{downcast_impls, upcast_impls}, constructors::constructor_methods, debug::derive_debug_with_spec, fold::derive_fold, fuzz::derive_fuzz, parse::derive_parse_with_spec, spec::FormalitySpec, visit::derive_visit }; pub fn term(spec: Option, mut input: DeriveInput) -> syn::Result { @@ -43,26 +36,15 @@ pub fn term(spec: Option, mut input: DeriveInput) -> syn::Result< } else { constructor_methods(synstructure::Structure::new(&input)) }; - remove_formality_attributes(&mut input); - - let derives: Vec = vec![ - quote!(Clone), - quote!(PartialEq), - quote!(Eq), - quote!(PartialOrd), - quote!(Ord), - quote!(Hash), - ] - .into_iter() - .chain(if customize.fuzz { - None + let fuzz_impl = if customize.fuzz { + Default::default() } else { - Some(quote!(bolero::TypeGenerator)) - }) - .collect(); + derive_fuzz(synstructure::Structure::new(&input)) + }; + remove_formality_attributes(&mut input); Ok(quote! { - #[derive(#(#derives),*)] + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #input #fold_impl @@ -70,6 +52,7 @@ pub fn term(spec: Option, mut input: DeriveInput) -> syn::Result< #parse_impl #debug_impl #term_impl + #fuzz_impl #(#downcast_impls)* #(#upcast_impls)* #constructors diff --git a/crates/formality-types/src/fuzz.rs b/crates/formality-types/src/fuzz.rs index 146f937f..13f28c80 100644 --- a/crates/formality-types/src/fuzz.rs +++ b/crates/formality-types/src/fuzz.rs @@ -5,76 +5,70 @@ //! [f]: https://rust-lang.github.io/a-mir-formality/formality_core/fuzzing.html use formality_core::{ - fuzz::{FuzzSingleton, SetGuard}, + fuzz::{FuzzCx, FuzzSingleton, SetGuard}, Map, }; use crate::grammar::{AdtId, AssociatedItemId, ParameterKind, TraitId}; -/// The "Fuzz Context" stores the set of ADTs/traits/associated types and their associated kinds. -/// It can be "installed" using `FuzzCz::install` and then when we generate types we will reference it. -/// -/// See the Formality Book [chapter on fuzzing][f] for more details. -/// -/// [f]: https://rust-lang.github.io/a-mir-formality/formality_core/fuzzing.html -pub struct FuzzCx { - pub adt_kinds: Map>, - pub trait_kinds: Map>, - pub associated_items: Map)>, -} - -/// Map from each `AdtId` to its parameter kinds. Modified by `FuzzCx::install` and referenced in the other methods. -static ADT_ID_MAP: FuzzSingleton>> = FuzzSingleton::new(); - -/// Map from each `TraitId` to its parameter kinds. Modified by `FuzzCx::install` and referenced in the other methods. -static TRAIT_ID_MAP: FuzzSingleton>> = FuzzSingleton::new(); - -/// Map from each `AssociatedItemId` to its parameter kinds. Modified by `FuzzCx::install` and referenced in the other methods. -static ASSOCIATED_ID_MAP: FuzzSingleton)>> = - FuzzSingleton::new(); - -/// Guard returned by `FuzzCx::install` to remove this context from scope. -pub struct FuzzCxGuard { - guards: Vec>, -} - -impl FuzzCx { - /// Setup a fuzzing session for fuzzing Rust types. - /// Returns a guard to uninstall the context. +/// Methods for installing/querying the context needed to fuzz Rust types. +pub trait RustTypesFuzzCx { + /// Install the context needed to fuzz Rust types. /// - /// Trying to fuzz a type instance will panic if this method has not been - /// called or if the returned guard has been dropped. - /// - /// The returned guard must be dropped before `FuzzCx::install` can be used again. - pub fn install(self) -> FuzzCxGuard { - let mut guards: Vec> = vec![]; - - let adt_ids: Vec = self.adt_kinds.iter().map(|(k, _v)| k).cloned().collect(); - guards.push(AdtId::fuzz_pool().set(adt_ids)); - - let trait_ids: Vec = self.trait_kinds.iter().map(|(k, _v)| k).cloned().collect(); - guards.push(TraitId::fuzz_pool().set(trait_ids)); - - guards.push(ADT_ID_MAP.set(self.adt_kinds)); - - guards.push(TRAIT_ID_MAP.set(self.trait_kinds)); + /// * `adt_kinds`: the set of ADT names and their affiliated kinds. + /// * `trait_kinds`: the set of trait names and their affiliated kinds. + /// * `associated_items`: the set of associated items and their trait / affiliated kinds + /// (these are the GAT parameters and do not include those of the trait). + fn install( + &mut self, + adt_kinds: Map>, + trait_kinds: Map>, + associated_items: Map)>, + ); + + /// Fetch the kinds of `adt_id` + fn adt_kinds(&self, adt_id: AdtId) -> Vec; + + /// Fetch the kinds of `trait_id` + fn trait_kinds(&self, trait_id: TraitId) -> Vec; + + /// Fetch the trait / kinds of `associated_item_id` + fn associated_item_kinds( + &self, + associated_item_id: AssociatedItemId, + ) -> (TraitId, Vec); +} - FuzzCxGuard { guards } +impl RustTypesFuzzCx for FuzzCx<'_> { + fn install( + &mut self, + adt_kinds: Map>, + trait_kinds: Map>, + associated_items: Map)>, + ) { + // Set the valid values for AdtIds, TraitIds, and AssociatedItemId: + self.set_values(adt_kinds.iter().map(|(k, _v)| k).cloned().collect()); + self.set_values(trait_kinds.iter().map(|(k, _v)| k).cloned().collect()); + self.set_values(associated_items.iter().map(|(k, _v)| k).cloned().collect()); + + // Set the maps that are fetchable below + self.set_key_values(adt_kinds); + self.set_key_values(trait_kinds); + self.set_key_values(associated_items); } - /// Access the installed map from adt-id to associated parameter kinds. - pub fn adt_id_map() -> &'static FuzzSingleton>> { - &ADT_ID_MAP + fn adt_kinds(&self, adt_id: AdtId) -> Vec { + self.key_value(&adt_id).unwrap() } - /// Access the installed map from trait-id to associated parameter kinds. - pub fn trait_id_map() -> &'static FuzzSingleton>> { - &TRAIT_ID_MAP + fn trait_kinds(&self, trait_id: TraitId) -> Vec { + self.key_value(&trait_id).unwrap() } - /// Access the installed map from associated item id to associated parameter kinds. - pub fn associated_id_map( - ) -> &'static FuzzSingleton)>> { - &ASSOCIATED_ID_MAP + fn associated_item_kinds( + &self, + associated_item_id: AssociatedItemId, + ) -> (TraitId, Vec) { + self.key_value(&trait_id).unwrap() } } diff --git a/crates/formality-types/src/grammar/ty/fuzz_impls.rs b/crates/formality-types/src/grammar/ty/fuzz_impls.rs index 196358e3..4f9fc809 100644 --- a/crates/formality-types/src/grammar/ty/fuzz_impls.rs +++ b/crates/formality-types/src/grammar/ty/fuzz_impls.rs @@ -1,53 +1,65 @@ -use formality_core::Upcast; +use formality_core::{fuzz::Fuzzable, Upcast}; use crate::{ - fuzz::FuzzCx, - grammar::{AssociatedItemId, Const}, + fuzz::RustTypesFuzzCx, + grammar::{AssociatedItemId, Const, TraitId}, }; -use super::{AssociatedTyName, Lt, ParameterKind, RigidName, RigidTy, Ty}; - -impl bolero::TypeGenerator for RigidTy { - fn generate(driver: &mut D) -> Option { - // This is customized so that we can be sure to generate types - // with property arity. - - // First create the name - let name: RigidName = driver.gen()?; - - // Find the right kinds for that name - let parameter_kinds = match &name { - RigidName::AdtId(adt_id) => FuzzCx::adt_id_map().get().get(adt_id)?.clone(), - RigidName::ScalarId(_) => vec![], - RigidName::Ref(_) => vec![ParameterKind::Lt, ParameterKind::Ty], - RigidName::Tuple(arity) => vec![ParameterKind::Ty; *arity], - RigidName::FnPtr(_) => return None, // FIXME - RigidName::FnDef(_) => return None, // FIXME - }; - - // Generate parameters of the correct kinds - let parameters = parameter_kinds - .iter() - .map(|k| match k { - ParameterKind::Ty => driver.gen::().upcast(), - ParameterKind::Lt => driver.gen::().upcast(), - ParameterKind::Const => driver.gen::().upcast(), - }) - .collect::>()?; +use super::{AssociatedTyName, Lt, ParameterKind, Parameters, RigidName, RigidTy, Ty}; + +impl Fuzzable for RigidTy { + fn estimate_cardinality(cx: &mut formality_core::fuzz::FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| { + guard.estimate_cardinality::() * guard.estimate_cardinality::() + }) + } + + fn fuzz(cx: &mut formality_core::fuzz::FuzzCx) -> Option { + cx.enter_fuzz(|guard| { + let name = guard.fuzz::()?; + + // Find the right kinds for that name + let parameter_kinds = match &name { + RigidName::AdtId(adt_id) => guard.adt_kinds(adt_id), + RigidName::ScalarId(_) => vec![], + RigidName::Ref(_) => vec![ParameterKind::Lt, ParameterKind::Ty], + RigidName::Tuple(arity) => vec![ParameterKind::Ty; *arity], + RigidName::FnPtr(_) => return None, // FIXME + RigidName::FnDef(_) => return None, // FIXME + }; - Some(RigidTy { name, parameters }) + // Generate parameters of the correct kinds + let parameters = parameter_kinds + .iter() + .map(|k| match k { + ParameterKind::Ty => guard.fuzz::().upcast(), + ParameterKind::Lt => guard.fuzz::().upcast(), + ParameterKind::Const => guard.fuzz::().upcast(), + }) + .collect::>()?; + + Some(RigidTy { name, parameters }) + }) } } -impl bolero::TypeGenerator for AssociatedTyName { - fn generate(driver: &mut D) -> Option { - // Create an associated item id and look up its associated information - let item_id: AssociatedItemId = driver.gen()?; - let (trait_id, parameter_kinds) = FuzzCx::associated_id_map().get().get(&item_id)?.clone(); - Some(AssociatedTyName { - trait_id, - item_id, - item_arity: parameter_kinds.len(), +impl Fuzzable for AssociatedTyName { + fn estimate_cardinality(cx: &mut formality_core::fuzz::FuzzCx) -> f64 { + cx.enter_estimate_cardinality::(|guard| { + guard.estimate_cardinality::() + }) + } + + fn fuzz(cx: &mut formality_core::fuzz::FuzzCx) -> Option { + cx.enter_fuzz::(|guard| { + let item_id: AssociatedItemId = guard.fuzz()?; + let (trait_id, parameter_kinds): (TraitId, Vec) = + guard.key_value(&item_id)?; + Some(AssociatedTyName { + trait_id, + item_id, + item_arity: parameter_kinds.len(), + }) }) } }