diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 89ae3ff0007..1708530d588 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2540,16 +2540,9 @@ def get_random_opts(): if has_flatten: print('avoiding multiple --flatten in a single command, due to exponential overhead') continue - if '--enable-multivalue' in FEATURE_OPTS and '--enable-reference-types' in FEATURE_OPTS: - print('avoiding --flatten due to multivalue + reference types not supporting it (spilling of non-nullable tuples)') - print('TODO: Resolving https://github.com/WebAssembly/binaryen/issues/4824 may fix this') - continue if '--enable-exception-handling' in FEATURE_OPTS: print('avoiding --flatten due to exception-handling not supporting it (requires blocks with results)') continue - if '--gc' not in FEATURE_OPTS: - print('avoiding --flatten due to GC not supporting it (spilling of non-nullable locals)') - continue if INITIAL_CONTENTS and os.path.getsize(INITIAL_CONTENTS) > 2000: print('avoiding --flatten due using a large amount of initial contents, which may blow up') continue diff --git a/src/ir/flat.h b/src/ir/flat.h index c4c8b219600..71b7426c14f 100644 --- a/src/ir/flat.h +++ b/src/ir/flat.h @@ -39,7 +39,7 @@ // (i32.const 1) // ) // -// Formally, this pass flattens in the precise sense of +// Formally, the Flatten pass flattens in the precise sense of // making the AST have these properties: // // 1. Aside from a local.set, the operands of an instruction must be a @@ -54,9 +54,8 @@ // values is prohibited already, but e.g. a block ending in unreachable, // which can normally be nested, is also disallowed). // -// Note: ref.as_non_null must be allowed in a nested position because we cannot -// spill it to a local - the result is non-null, which is not allowable in a -// local. +// This header defines the concept of flatness, and utilities for verifying it, +// which each pass that assumes flat form should use. // #ifndef wasm_ir_flat_h @@ -76,12 +75,15 @@ inline void verifyFlatness(Function* func) { void visitExpression(Expression* curr) { if (Properties::isControlFlowStructure(curr)) { verify(!curr->type.isConcrete(), - "control flow structures must not flow values"); + "control flow structures must not flow values", + curr); } else if (auto* set = curr->dynCast()) { verify(!set->isTee() || set->type == Type::unreachable, - "tees are not allowed, only sets"); + "tees are not allowed, only sets", + set); verify(!Properties::isControlFlowStructure(set->value), - "set values cannot be control flow"); + "set values cannot be control flow", + set); } else { for (auto* child : ChildIterator(curr)) { bool isRefAsNonNull = @@ -90,15 +92,16 @@ inline void verifyFlatness(Function* func) { child->is() || child->is() || isRefAsNonNull, "instructions must only have constant expressions, local.get, " - "or unreachable as children"); + "or unreachable as children", + curr); } } } - void verify(bool condition, const char* message) { + void verify(bool condition, const char* message, Expression* expr) { if (!condition) { Fatal() << "IR must be flat: run --flatten beforehand (" << message - << ", in " << getFunction()->name << ')'; + << ", in " << getFunction()->name << ") " << *expr; } } }; @@ -107,7 +110,8 @@ inline void verifyFlatness(Function* func) { verifier.walkFunction(func); verifier.setFunction(func); verifier.verify(!func->body->type.isConcrete(), - "function bodies must not flow values"); + "function bodies must not flow values", + nullptr); } inline void verifyFlatness(Module* module) { diff --git a/src/passes/Flatten.cpp b/src/passes/Flatten.cpp index 1c2cfbcd536..28140f06c34 100644 --- a/src/passes/Flatten.cpp +++ b/src/passes/Flatten.cpp @@ -35,9 +35,11 @@ // // The tuple has a non-nullable type, and so it cannot currently be set to a // local, but in principle there's no reason it couldn't be. For now, error on -// this. +// this. Note: running TupleOptimization can work around this, by removing such +// tuples. #include +#include #include #include #include @@ -49,6 +51,98 @@ namespace wasm { +// Lowers instructions that Flatten cannot directly handle. It is simpler to +// first lower them to instructions that it can. +struct LowerUnflattenable : public PostWalker { + Builder builder; + const PassOptions& options; + + LowerUnflattenable(Module& wasm, const PassOptions& options) + : builder(wasm), options(options) {} + + void visitBrOn(BrOn* curr) { + if (curr->type == Type::unreachable) { + replaceUnreachableWithDrops(); + return; + } + + // All ops stash the ref to a temp var. + auto refType = curr->ref->type; + auto refTemp = builder.addVar(getFunction(), refType); + // All ops will use this refTee, and more gets. + auto* refTee = builder.makeLocalTee(refTemp, curr->ref, refType); + auto getRef = [&]() { return builder.makeLocalGet(refTemp, refType); }; + + // The overall shape we emit is to check a condition, branch if so with + // some value, and if not, flow out something: + // + // if (condition) { + // br(brValue); + // } + // flowValue + // + // Each op fills in those things. + Expression* condition = nullptr; // uses refTee + Expression* brValue = nullptr; + Expression* flowValue = nullptr; + bool flip = false; // whether to flip the condition. + + switch (curr->op) { + case BrOnNull: { + condition = builder.makeRefIsNull(refTee); + brValue = nullptr; + flowValue = builder.makeRefAs(RefAsNonNull, getRef()); + break; + } + case BrOnNonNull: { + condition = builder.makeRefIsNull(refTee); + flip = true; + brValue = builder.makeRefAs(RefAsNonNull, getRef()); + // No value flos out. + break; + } + case BrOnCast: { + condition = builder.makeRefTest(refTee, curr->castType); + brValue = builder.makeRefCast(getRef(), curr->castType); + flowValue = getRef(); + break; + } + case BrOnCastFail: { + condition = builder.makeRefTest(refTee, curr->castType); + flip = true; + brValue = getRef(); + flowValue = builder.makeRefCast(getRef(), curr->castType); + break; + } + case BrOnCastDescEq: + case BrOnCastDescEqFail: { + WASM_UNREACHABLE("TODO"); + } + } + + if (flip) { + condition = builder.makeUnary(EqZInt32, condition); + } + + auto* br = builder.makeBreak(curr->name, brValue); + Expression* result = builder.makeIf(condition, br); + if (flowValue) { + result = builder.makeSequence(result, flowValue); + } + replaceCurrent(result); + } + + void replaceUnreachableWithDrops() { + Builder builder(*getModule()); + + getDroppedChildrenAndAppend(getCurrent(), + *getModule(), + options, + builder.makeUnreachable(), + DropMode::IgnoreParentEffects); + } +}; + // We use the following algorithm: we maintain a list of "preludes", code // that runs right before an expression. When we visit an expression we // must handle it and its preludes. If the expression has side effects, @@ -333,7 +427,7 @@ struct Flatten } } - if (curr->is() || curr->is()) { + if (curr->is()) { Fatal() << "Unsupported instruction for Flatten: " << getExpressionName(curr); } @@ -368,7 +462,15 @@ struct Flatten } } - void visitFunction(Function* curr) { + void doWalkFunction(Function* curr) { + // Lower things before the main walk. + LowerUnflattenable(*getModule(), getPassOptions()).walkFunction(curr); + + WalkerPass< + ExpressionStackWalker>>:: + doWalkFunction(curr); + + // Finish up. auto* originalBody = curr->body; // if the body is a block with a result, turn that into a return if (curr->body->type.isConcrete()) { diff --git a/src/passes/ReReloop.cpp b/src/passes/ReReloop.cpp index f31736940cf..57b164e0ee6 100644 --- a/src/passes/ReReloop.cpp +++ b/src/passes/ReReloop.cpp @@ -24,6 +24,7 @@ #include #include "cfg/Relooper.h" +#include "ir/find_all.h" #include "ir/flat.h" #include "ir/utils.h" #include "pass.h" @@ -284,7 +285,7 @@ struct ReReloop final : public Pass { ReturnTask::handle(*this, ret); } else if (auto* un = curr->dynCast()) { UnreachableTask::handle(*this, un); - } else if (curr->is() || curr->is() || curr->is()) { + } else if (curr->is()) { Fatal() << "ReReloop does not support EH instructions yet"; } else { // not control flow, so just a simple element @@ -298,6 +299,11 @@ struct ReReloop final : public Pass { } void runOnFunction(Module* module, Function* function) override { + if (FindAll(function->body).list.size()) { + std::cout << "skip " << function->name << '\n'; + return; + } + Flat::verifyFlatness(function); // since control flow is flattened, this is pretty simple diff --git a/test/lit/passes/flatten-gc.wast b/test/lit/passes/flatten-gc.wast new file mode 100644 index 00000000000..2a235be8ef0 --- /dev/null +++ b/test/lit/passes/flatten-gc.wast @@ -0,0 +1,352 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. + +;; RUN: wasm-opt %s --flatten -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $A (sub (struct))) + (type $A (sub (struct))) + + ;; CHECK: (type $B (sub $A (struct))) + (type $B (sub $A (struct))) + + ;; CHECK: (func $br_on_null (type $2) (param $x (ref null $A)) (result (ref $A)) + ;; CHECK-NEXT: (local $1 (ref null $A)) + ;; CHECK-NEXT: (local $2 (ref null $A)) + ;; CHECK-NEXT: (local $3 (ref null $A)) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 (ref null $A)) + ;; CHECK-NEXT: (local $6 (ref $A)) + ;; CHECK-NEXT: (local $7 (ref $A)) + ;; CHECK-NEXT: (local $8 (ref $A)) + ;; CHECK-NEXT: (local $9 (ref null $A)) + ;; CHECK-NEXT: (local $10 (ref $A)) + ;; CHECK-NEXT: (block $non-null + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $block) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $10 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on_null (param $x (ref null $A)) (result (ref $A)) + (block $non-null (result (ref $A)) + (block $block + (return + (br_on_null $block + (local.get $x) + ) + ) + ) + (unreachable) + ) + ) + + ;; CHECK: (func $br_on_non_null (type $2) (param $x (ref null $A)) (result (ref $A)) + ;; CHECK-NEXT: (local $1 (ref null $A)) + ;; CHECK-NEXT: (local $2 (ref null $A)) + ;; CHECK-NEXT: (local $3 (ref null $A)) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (local $6 (ref null $A)) + ;; CHECK-NEXT: (local $7 (ref $A)) + ;; CHECK-NEXT: (local $8 (ref null $A)) + ;; CHECK-NEXT: (local $9 (ref $A)) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $block) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on_non_null (param $x (ref null $A)) (result (ref $A)) + (block $block (result (ref $A)) + (br_on_non_null $block + (local.get $x) + ) + (unreachable) + ) + ) + + ;; CHECK: (func $br_on_cast (type $3) (param $x (ref $A)) (result (ref $B)) + ;; CHECK-NEXT: (local $1 (ref $A)) + ;; CHECK-NEXT: (local $2 (ref $A)) + ;; CHECK-NEXT: (local $3 (ref $A)) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 (ref $A)) + ;; CHECK-NEXT: (local $6 (ref $B)) + ;; CHECK-NEXT: (local $7 (ref null $B)) + ;; CHECK-NEXT: (local $8 (ref $A)) + ;; CHECK-NEXT: (local $9 (ref $A)) + ;; CHECK-NEXT: (local $10 (ref $A)) + ;; CHECK-NEXT: (local $11 (ref $B)) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (ref.test (ref $B) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $block) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (local.get $8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $10 + ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $11 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $11) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on_cast (param $x (ref $A)) (result (ref $B)) + (block $block (result (ref $B)) + (br_on_cast $block (ref $A) (ref $B) + (local.get $x) + ) + (unreachable) + ) + ) + + ;; CHECK: (func $br_on_cast_fail (type $4) (param $x (ref $A)) (result (ref null $B)) + ;; CHECK-NEXT: (local $1 (ref $A)) + ;; CHECK-NEXT: (local $2 (ref $A)) + ;; CHECK-NEXT: (local $3 (ref $A)) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (local $6 (ref $A)) + ;; CHECK-NEXT: (local $7 anyref) + ;; CHECK-NEXT: (local $8 (ref $A)) + ;; CHECK-NEXT: (local $9 (ref $A)) + ;; CHECK-NEXT: (local $10 (ref $B)) + ;; CHECK-NEXT: (local $11 (ref $B)) + ;; CHECK-NEXT: (local $12 (ref $B)) + ;; CHECK-NEXT: (local $13 anyref) + ;; CHECK-NEXT: (local $14 (ref null $B)) + ;; CHECK-NEXT: (local $15 (ref null $B)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (ref.test (ref $B) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $block) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $10 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $11 + ;; CHECK-NEXT: (local.get $10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $12 + ;; CHECK-NEXT: (local.get $11) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $12) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $13 + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $13) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $15 + ;; CHECK-NEXT: (local.get $14) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $15) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on_cast_fail (param $x (ref $A)) (result (ref null $B)) + (block $block (result anyref) + (return + (br_on_cast_fail $block (ref $A) (ref $B) + (local.get $x) + ) + ) + (unreachable) + ) + (unreachable) + ) +)