diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 6954d8cf029..07ae5d702ae 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -19,6 +19,7 @@ #include "ir/iteration.h" #include "ir/local-structural-dominance.h" #include "ir/module-utils.h" +#include "ir/subtype-exprs.h" #include "ir/subtypes.h" #include "ir/type-updating.h" #include "support/string.h" @@ -1733,16 +1734,66 @@ void TranslateToFuzzReader::mutate(Function* func) { // reasonable chance of making some changes. percentChance = std::max(percentChance, Index(3)); + // First, find things to replace and their types. SubtypingDiscoverer needs to + // do this in a single, full walk (as types of children depend on parents, and + // even block targets). + struct Finder + : public ControlFlowWalker> { + // Maps children we can replace to the types we can replace them with. We + // only store nontrivial ones (i.e., where the type is not just the child's + // type). + std::unordered_map childTypes; + + // We only care about constraints on Expression* things. + void noteSubtype(Type sub, Type super) {} + void noteSubtype(HeapType sub, HeapType super) {} + + void noteSubtype(Type sub, Expression* super) { + // The expression must be a supertype of a fixed type. Nothing to do. + } + void noteSubtype(Expression* sub, Type super) { + if (super.isRef() && sub->type != super) { + // This is a nontrivial opportunity to replace sub with a given type. + childTypes[sub] = super; + } + } + void noteSubtype(Expression* sub, Expression* super) { + noteSubtype(sub, super->type); + } + void noteNonFlowSubtype(Expression* sub, Type super) { + noteSubtype(sub, super); + } + + // TODO: Many casts can accept the top type. We may need to use visit*(), to + // handle each expression class separately. + void noteCast(HeapType src, HeapType dst) {} + void noteCast(Expression* src, Type dst) {} + void noteCast(Expression* src, Expression* dst) {} + } finder; + + if (oneIn(2)) { + // |finder| reads the IR, and it must be totally valid - e.g. breaks have a + // proper break target - or else we'd hit internal errors. Fix it up first. + // (Otherwise, fixing it up is done once after mutation, and in that case + // we can mutate the IR in simple ways but not read it using + // useSubtypingDiscoverer). We avoid always doing this second fixup as it + // may bias the code in some ways. + fixAfterChanges(func); + finder.walkFunctionInModule(func, &wasm); + } + + // Next, modify things. struct Modder : public PostWalker> { TranslateToFuzzReader& parent; Index percentChance; + Finder& finder; // Whether to replace with unreachable. This can lead to less code getting // executed, so we don't want to do it all the time even in a big function. bool allowUnreachable; - Modder(TranslateToFuzzReader& parent, Index percentChance) - : parent(parent), percentChance(percentChance) { + Modder(TranslateToFuzzReader& parent, Index percentChance, Finder& finder) + : parent(parent), percentChance(percentChance), finder(finder) { // If the parent allows it then sometimes replace with an unreachable, and // sometimes not. Even if we allow it, only do it in certain functions // (half the time) and only do it rarely (see below). @@ -1750,136 +1801,152 @@ void TranslateToFuzzReader::mutate(Function* func) { } void visitExpression(Expression* curr) { - if (parent.upTo(100) < percentChance && - parent.canBeArbitrarilyReplaced(curr)) { - // We can replace in various modes, see below. Generate a random number - // up to 100 to help us there. - int mode = parent.upTo(100); - - if (allowUnreachable && mode < 5) { - replaceCurrent(parent.make(Type::unreachable)); - return; + // See if we want to replace it. + if (!parent.canBeArbitrarilyReplaced(curr) || + parent.upTo(100) >= percentChance) { + return; + } + + // Find the type to replace with. + auto type = curr->type; + if (type.isRef()) { + auto iter = finder.childTypes.find(curr); + if (iter != finder.childTypes.end()) { + type = iter->second; + // We can only be given a less-refined type (certainly we can replace + // curr with its own type). + assert(Type::isSubType(curr->type, type)); + // We only store an interesting non-trivial type. + assert(type != curr->type); } + } - // For constants, perform only a small tweaking in some cases. - // TODO: more minor tweaks to immediates, like making a load atomic or - // not, changing an offset, etc. - if (auto* c = curr->dynCast()) { - if (mode < 50) { - c->value = parent.tweak(c->value); - } else { - // Just replace the entire thing. - replaceCurrent(parent.make(curr->type)); - } - return; + // We can replace in various modes, see below. Generate a random number + // up to 100 to help us there. + int mode = parent.upTo(100); + + if (allowUnreachable && mode < 5) { + replaceCurrent(parent.make(Type::unreachable)); + return; + } + + // For constants, perform only a small tweaking in some cases. + // TODO: more minor tweaks to immediates, like making a load atomic or + // not, changing an offset, etc. + if (auto* c = curr->dynCast()) { + if (mode < 50) { + c->value = parent.tweak(c->value); + } else { + // Just replace the entire thing. + replaceCurrent(parent.make(type)); } + return; + } - // Generate a replacement for the expression, and by default replace all - // of |curr| (including children) with that replacement, but in some - // cases we can do more subtle things. + // Generate a replacement for the expression, and by default replace all + // of |curr| (including children) with that replacement, but in some + // cases we can do more subtle things. + // + // Note that such a replacement is not always valid due to nesting of + // labels, but we'll fix that up later. Note also that make() picks a + // subtype, so this has a chance to replace us with anything that is + // valid to put here. + auto* rep = parent.make(type); + if (mode < 33 && rep->type != Type::none) { + // This has a non-none type. Replace the output, keeping the + // expression and its children in a drop. This "interposes" between + // this expression and its parent, something like this: // - // Note that such a replacement is not always valid due to nesting of - // labels, but we'll fix that up later. Note also that make() picks a - // subtype, so this has a chance to replace us with anything that is - // valid to put here. - auto* rep = parent.make(curr->type); - if (mode < 33 && rep->type != Type::none) { - // This has a non-none type. Replace the output, keeping the - // expression and its children in a drop. This "interposes" between - // this expression and its parent, something like this: - // - // (D - // (A - // (B) - // (C) - // ) - // ) - //// - // => ;; keep A, replace it in the parent + // (D + // (A + // (B) + // (C) + // ) + // ) + //// + // => ;; keep A, replace it in the parent + // + // (D + // (block + // (drop + // (A + // (B) + // (C) + // ) + // ) + // (NEW) + // ) + // ) + // + // We also sometimes try to insert A as a child of NEW, so we actually + // interpose directly: + // + // (D + // (NEW + // (A + // (B) + // (C) + // ) + // ) + // ) + // + // We do not do that all the time, as inserting a drop is actually an + // important situation to test: the drop makes the output of A unused, + // which may let optimizations remove it. + if ((mode & 1) && replaceChildWith(rep, curr)) { + // We managed to replace one of the children with curr, and have + // nothing more to do. + } else { + // Drop curr and append. + rep = parent.builder.makeSequence(parent.builder.makeDrop(curr), rep); + } + } else if (mode >= 66 && !Properties::isControlFlowStructure(curr)) { + ChildIterator children(curr); + auto numChildren = children.getNumChildren(); + if (numChildren > 0 && numChildren < 5) { + // This is a normal (non-control-flow) expression with at least one + // child (and not an excessive amount of them; see the processing + // below). "Interpose" between the children and this expression by + // keeping them and replacing the parent |curr|. We do this by + // generating drops of the children, like this: // - // (D - // (block - // (drop - // (A - // (B) - // (C) - // ) - // ) - // (NEW) - // ) - // ) + // (A + // (B) + // (C) + // ) // - // We also sometimes try to insert A as a child of NEW, so we actually - // interpose directly: + // => ;; keep children, replace A // - // (D - // (NEW - // (A - // (B) - // (C) - // ) - // ) - // ) + // (block + // (drop (B)) + // (drop (C)) + // (NEW) + // ) // - // We do not do that all the time, as inserting a drop is actually an - // important situation to test: the drop makes the output of A unused, - // which may let optimizations remove it. - if ((mode & 1) && replaceChildWith(rep, curr)) { - // We managed to replace one of the children with curr, and have - // nothing more to do. - } else { - // Drop curr and append. - rep = - parent.builder.makeSequence(parent.builder.makeDrop(curr), rep); - } - } else if (mode >= 66 && !Properties::isControlFlowStructure(curr)) { - ChildIterator children(curr); - auto numChildren = children.getNumChildren(); - if (numChildren > 0 && numChildren < 5) { - // This is a normal (non-control-flow) expression with at least one - // child (and not an excessive amount of them; see the processing - // below). "Interpose" between the children and this expression by - // keeping them and replacing the parent |curr|. We do this by - // generating drops of the children, like this: - // - // (A - // (B) - // (C) - // ) - // - // => ;; keep children, replace A - // - // (block - // (drop (B)) - // (drop (C)) - // (NEW) - // ) - // - auto* block = parent.builder.makeBlock(); - for (auto* child : children) { - // Only drop the child if we can't replace it as one of NEW's - // children. This does a linear scan of |rep| which is the reason - // for the above limit on the number of children. - if (!replaceChildWith(rep, child)) { - block->list.push_back(parent.builder.makeDrop(child)); - } + auto* block = parent.builder.makeBlock(); + for (auto* child : children) { + // Only drop the child if we can't replace it as one of NEW's + // children. This does a linear scan of |rep| which is the reason + // for the above limit on the number of children. + if (!replaceChildWith(rep, child)) { + block->list.push_back(parent.builder.makeDrop(child)); } + } - if (!block->list.empty()) { - // We need the block, that is, we did not find a place for all the - // children. - block->list.push_back(rep); - block->finalize(); - rep = block; - } + if (!block->list.empty()) { + // We need the block, that is, we did not find a place for all the + // children. + block->list.push_back(rep); + block->finalize(); + rep = block; } } - replaceCurrent(rep); } + replaceCurrent(rep); } }; - Modder modder(*this, percentChance); + Modder modder(*this, percentChance, finder); modder.walkFunctionInModule(func, &wasm); } diff --git a/test/passes/fuzz_metrics_noprint.bin.txt b/test/passes/fuzz_metrics_noprint.bin.txt index 50632e024d7..baa6a426640 100644 --- a/test/passes/fuzz_metrics_noprint.bin.txt +++ b/test/passes/fuzz_metrics_noprint.bin.txt @@ -1,34 +1,34 @@ Metrics total - [exports] : 65 - [funcs] : 93 + [exports] : 32 + [funcs] : 42 [globals] : 7 [imports] : 4 [memories] : 1 [memory-data] : 23 - [table-data] : 25 + [table-data] : 9 [tables] : 1 [tags] : 0 - [total] : 6794 - [vars] : 256 - Binary : 454 - Block : 1202 - Break : 188 - Call : 205 - CallIndirect : 61 - Const : 1132 - Drop : 89 - GlobalGet : 635 - GlobalSet : 487 - If : 378 - Load : 88 - LocalGet : 406 - LocalSet : 341 - Loop : 148 - Nop : 107 - RefFunc : 25 - Return : 58 - Select : 52 - Store : 41 - Unary : 451 - Unreachable : 246 + [total] : 7716 + [vars] : 107 + Binary : 512 + Block : 1312 + Break : 299 + Call : 245 + CallIndirect : 49 + Const : 1197 + Drop : 95 + GlobalGet : 659 + GlobalSet : 465 + If : 416 + Load : 140 + LocalGet : 625 + LocalSet : 450 + Loop : 188 + Nop : 87 + RefFunc : 9 + Return : 75 + Select : 67 + Store : 45 + Unary : 547 + Unreachable : 234 diff --git a/test/passes/fuzz_metrics_passes_noprint.bin.txt b/test/passes/fuzz_metrics_passes_noprint.bin.txt index 9994355a595..ffcba736f5a 100644 --- a/test/passes/fuzz_metrics_passes_noprint.bin.txt +++ b/test/passes/fuzz_metrics_passes_noprint.bin.txt @@ -1,35 +1,35 @@ Metrics total - [exports] : 38 - [funcs] : 59 + [exports] : 17 + [funcs] : 25 [globals] : 4 [imports] : 6 [memories] : 1 [memory-data] : 20 - [table-data] : 28 + [table-data] : 9 [tables] : 1 [tags] : 0 - [total] : 9393 - [vars] : 189 - Binary : 651 - Block : 1535 - Break : 316 - Call : 296 - CallIndirect : 91 - Const : 1670 - Drop : 66 - GlobalGet : 650 - GlobalSet : 582 - If : 506 - Load : 149 - LocalGet : 823 - LocalSet : 497 - Loop : 232 - Nop : 114 - RefFunc : 28 - Return : 83 - Select : 75 - Store : 71 - Switch : 7 - Unary : 659 - Unreachable : 292 + [total] : 3774 + [vars] : 91 + Binary : 238 + Block : 655 + Break : 105 + Call : 135 + CallIndirect : 23 + Const : 703 + Drop : 103 + GlobalGet : 258 + GlobalSet : 230 + If : 211 + Load : 44 + LocalGet : 285 + LocalSet : 163 + Loop : 90 + Nop : 64 + RefFunc : 9 + Return : 41 + Select : 24 + Store : 19 + Switch : 2 + Unary : 256 + Unreachable : 116 diff --git a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt index 1ffd3eb3d11..f92366af55d 100644 --- a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt +++ b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt @@ -1,51 +1,54 @@ Metrics total - [exports] : 18 - [funcs] : 24 + [exports] : 8 + [funcs] : 12 [globals] : 26 [imports] : 12 [memories] : 1 [memory-data] : 16 - [table-data] : 15 + [table-data] : 1 [tables] : 2 [tags] : 2 - [total] : 882 - [vars] : 54 - ArrayNewFixed : 7 - AtomicCmpxchg : 1 - Binary : 38 - Block : 172 + [total] : 724 + [vars] : 58 + ArrayNew : 1 + ArrayNewFixed : 4 + Binary : 31 + Block : 141 + BrOn : 1 Break : 9 - Call : 37 - Const : 180 - Drop : 72 - GlobalGet : 72 - GlobalSet : 60 - If : 33 - Load : 5 + Call : 28 + Const : 166 + Drop : 69 + GlobalGet : 47 + GlobalSet : 34 + I31Get : 2 + If : 20 + Load : 6 LocalGet : 8 - LocalSet : 9 - Loop : 6 - Nop : 7 - Pop : 2 - RefAs : 1 - RefEq : 4 - RefFunc : 15 - RefI31 : 8 - RefIsNull : 1 - RefNull : 10 - Return : 5 - SIMDExtract : 4 - Select : 1 - StringConst : 6 + LocalSet : 17 + Loop : 5 + Nop : 2 + Pop : 5 + RefCast : 1 + RefEq : 5 + RefFunc : 2 + RefI31 : 14 + RefNull : 9 + Return : 6 + SIMDExtract : 1 + Select : 3 + Store : 1 + StringConst : 9 + StringEncode : 1 StringEq : 1 StringMeasure : 1 - StringWTF16Get : 1 - StructNew : 13 - Throw : 1 - Try : 2 - TryTable : 2 - TupleExtract : 1 - TupleMake : 13 - Unary : 43 - Unreachable : 31 + StructNew : 11 + TableSet : 1 + Throw : 2 + Try : 5 + TryTable : 5 + TupleExtract : 2 + TupleMake : 7 + Unary : 24 + Unreachable : 17