Implement rec groups and struct mutators#12538
Implement rec groups and struct mutators#12538fitzgen merged 9 commits intobytecodealliance:mainfrom
rec groups and struct mutators#12538Conversation
Subscribe to Label Actioncc @fitzgen DetailsThis issue or pull request has been labeled: "fuzzing"Thus the following users have been cc'd because of the following labels:
To subscribe or unsubscribe from this label, edit the |
fitzgen
left a comment
There was a problem hiding this comment.
Thanks, just a few little things before merging.
| // Define a mutation that adds an empty struct type to an existing (rec ...) group. | ||
| if !c.shrink() | ||
| && !ops.types.rec_groups.is_empty() | ||
| && ops.types.type_defs.len() < ops.limits.max_types as usize | ||
| { | ||
| c.mutation(|ctx| { | ||
| // Pick a random rec group. | ||
| if let Some(group_id) = ctx.rng().choose(&ops.types.rec_groups).copied() { | ||
| // Insert new struct with next type id in the chosen rec group. | ||
| let new_tid = ops.types.next_type_id(); | ||
| ops.types.insert_empty_struct(new_tid, group_id); | ||
| log::debug!("Added empty struct type {new_tid:?} to rec group {group_id:?}"); | ||
| } | ||
| Ok(()) | ||
| })?; | ||
| } |
There was a problem hiding this comment.
Can we pull each of the mutations out into their own helper functions? This method is way too big after adding all these new mutations inline.
Something like this:
impl GcOpsMutator {
fn add_new_struct_type_to_rec_group(
&self,
c: &mut Candidates<'_>,
ops: &mut GcOps,
) -> mutatis::Result<()> {
if c.shrink()
|| ops.types.rec_groups.is_empty()
|| ops.types.type_defs.len() >= usize::try_from(ops.limits.max_types).unwrap()
{
return Ok(());
}
c.mutation(|ctx| {
let group_id = ctx.rng().choose(&ops.types.rec_groups)
.expect("already checked for empty rec groups above");
let new_tid = ops.types.next_type_id();
ops.types.insert_empty_struct(new_tid, group_id);
log::debug!("Added empty struct type {new_tid:?} to rec group {group_id:?}");
Ok(())
})
}
// etc... for the other mutations
}
impl Mutate<GcOps> for GcOpsMutator {
fn mutate(&mut self, c: &mut Candidates<'_>, ops: &mut GcOps) -> mutatis::Result<()> {
self.add_gc_op(c, ops)?;
self.remove_gc_op(c, ops)?;
self.add_new_struct_type_to_rec_group(c, ops)?;
// etc...
Ok(())
}
}| { | ||
| c.mutation(|ctx| { | ||
| // Pick a random rec group. | ||
| if let Some(group_id) = ctx.rng().choose(&ops.types.rec_groups).copied() { |
There was a problem hiding this comment.
We've already checked for the case where rec_groups.is_empty() above, so we can just unwrap/expect here, instead of checking that it is non-empty again.
| if !ops.types.type_defs.is_empty() { | ||
| c.mutation(|ctx| { | ||
| // Pick a random struct type. | ||
| if let Some(tid) = ctx.rng().choose(ops.types.type_defs.keys()).copied() { |
There was a problem hiding this comment.
Same thing about double-checking for emptiness. Here and elsewhere.
| /// Returns a fresh rec-group id one past the current maximum. | ||
| pub fn next_rec_group_id(&self) -> RecGroupId { | ||
| RecGroupId( | ||
| self.rec_groups | ||
| .iter() | ||
| .next_back() | ||
| .map(|g| g.0) | ||
| .unwrap_or(0) | ||
| .saturating_add(1), | ||
| ) | ||
| } | ||
|
|
||
| /// Returns a fresh type id one past the current maximum. | ||
| pub fn next_type_id(&self) -> TypeId { | ||
| TypeId( | ||
| self.type_defs | ||
| .keys() | ||
| .next_back() | ||
| .map(|id| id.0) | ||
| .unwrap_or(0) | ||
| .saturating_add(1), | ||
| ) | ||
| } |
There was a problem hiding this comment.
I think saturating_add isn't good enough here. The fuzzer could mutate the serialized representation of Types to make a type or rec group have its max id, at which point these methods and their saturating_add will saturate and return that same id.
You could do something like the following to choose either max(keys) + 1 or else the first gap in the keys:
let mut last_id = None;
let mut keys = self.type_defs.keys().rev();
let next_id = 'outer: {
while let Some(key) = keys.next() {
match (key.0.checked_add(1), last_key) {
(Some(x), None) => break 'outer TypeId(x),
(Some(x), Some(y)) if x != y => break 'outer TypeId(x),
(Some(_), Some(_)) | (None, _) => {
last_id = Some(key.0);
}
}
}
match last_id {
None => TypeId(0),
Some(x) => TypeId(x - 1),
}
};
debug_assert!(!self.type_defs.contains_key(next_id));
next_idBut this is kinda complicated for what it is doing and does devolve into an
The other thing we could do here is just take an rng: &mut mutatis::Rng parameter and do something like
for _ in 0..1000 {
let id = TypeId(rng.gen_u32());
if !self.type_defs.contains_key(&id) {
return id
}
}
panic!("failed to generate a new TypeId in 1000 iterations; bad RNG?")which should be good enough.
| if num_types == 0 { | ||
| unreachable!( | ||
| "typed struct requirement with num_types == 0; op should have been removed" | ||
| ); |
There was a problem hiding this comment.
This can be simplified to
debug_assert_ne!(num_types, 0, "...");…n the suggested reviews
|
Thanks @fitzgen! Ready for the review! |
Mutators added for
(rec ...)groups and struct types(rec ...)group(rec ...)group(rec ...)group(rec ...)group to another(rec ...)group(rec ...)group(rec ...)groups(rec ...)group in two, if possibleI have implemented the above mutators. I will keep updating/refining them as we add more features to recursive groups and struct definitions. Ready for the review! Thanks!
cc @fitzgen @eeide