diff options
-rw-r--r-- | src/ir/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/ir/stack-utils.cpp | 292 | ||||
-rw-r--r-- | src/ir/stack-utils.h | 196 | ||||
-rw-r--r-- | src/wasm-type.h | 30 | ||||
-rw-r--r-- | test/example/stack-utils.cpp | 443 | ||||
-rw-r--r-- | test/example/stack-utils.txt | 17 |
6 files changed, 976 insertions, 3 deletions
diff --git a/src/ir/CMakeLists.txt b/src/ir/CMakeLists.txt index 70a57a4ab..5555ef98b 100644 --- a/src/ir/CMakeLists.txt +++ b/src/ir/CMakeLists.txt @@ -4,6 +4,7 @@ set(ir_SOURCES ExpressionManipulator.cpp LocalGraph.cpp ReFinalize.cpp + stack-utils.cpp ${ir_HEADERS} ) add_library(ir OBJECT ${ir_SOURCES}) diff --git a/src/ir/stack-utils.cpp b/src/ir/stack-utils.cpp new file mode 100644 index 000000000..ef871040b --- /dev/null +++ b/src/ir/stack-utils.cpp @@ -0,0 +1,292 @@ +/* + * Copyright 2020 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "stack-utils.h" + +namespace wasm { + +namespace StackUtils { + +void removeNops(Block* block) { + size_t newIndex = 0; + for (size_t i = 0, size = block->list.size(); i < size; ++i) { + if (!block->list[i]->is<Nop>()) { + block->list[newIndex++] = block->list[i]; + } + } + block->list.resize(newIndex); +} + +} // namespace StackUtils + +StackSignature::StackSignature(Expression* expr) { + params = Type::none; + if (Properties::isControlFlowStructure(expr)) { + if (expr->is<If>()) { + params = Type::i32; + } + } else { + std::vector<Type> inputs; + for (auto* child : ChildIterator(expr)) { + assert(child->type.isConcrete()); + // Children might be tuple pops, so expand their types + inputs.insert(inputs.end(), child->type.begin(), child->type.end()); + } + params = Type(inputs); + } + if (expr->type == Type::unreachable) { + unreachable = true; + results = Type::none; + } else { + unreachable = false; + results = expr->type; + } +} + +bool StackSignature::composes(const StackSignature& next) const { + auto checked = std::min(results.size(), next.params.size()); + return std::equal(results.end() - checked, + results.end(), + next.params.end() - checked, + [](const Type& produced, const Type& consumed) { + return Type::isSubType(produced, consumed); + }); +} + +bool StackSignature::satisfies(Signature sig) const { + if (sig.params.size() < params.size() || + sig.results.size() < results.size()) { + // Not enough values provided or too many produced + return false; + } + bool paramSuffixMatches = + std::equal(params.begin(), + params.end(), + sig.params.end() - params.size(), + [](const Type& consumed, const Type& provided) { + return Type::isSubType(provided, consumed); + }); + if (!paramSuffixMatches) { + return false; + } + bool resultSuffixMatches = + std::equal(results.begin(), + results.end(), + sig.results.end() - results.size(), + [](const Type& produced, const Type& expected) { + return Type::isSubType(produced, expected); + }); + if (!resultSuffixMatches) { + return false; + } + if (unreachable) { + // The unreachable can consume any additional provided params and produce + // any additional expected results, so we are done. + return true; + } + // Any additional provided params will pass through untouched, so they must be + // equivalent to the additional produced results. + return std::equal(sig.params.begin(), + sig.params.end() - params.size(), + sig.results.begin(), + sig.results.end() - results.size(), + [](const Type& produced, const Type& expected) { + return Type::isSubType(produced, expected); + }); +} + +StackSignature& StackSignature::operator+=(const StackSignature& next) { + assert(composes(next)); + std::vector<Type> stack(results.begin(), results.end()); + size_t required = next.params.size(); + // Consume stack values according to next's parameters + if (stack.size() >= required) { + stack.resize(stack.size() - required); + } else { + if (!unreachable) { + // Prepend the unsatisfied params of `next` to the current params + size_t unsatisfied = required - stack.size(); + std::vector<Type> newParams(next.params.begin(), + next.params.begin() + unsatisfied); + newParams.insert(newParams.end(), params.begin(), params.end()); + params = Type(newParams); + } + stack.clear(); + } + // Add stack values according to next's results + if (next.unreachable) { + results = next.results; + unreachable = true; + } else { + stack.insert(stack.end(), next.results.begin(), next.results.end()); + results = Type(stack); + } + return *this; +} + +StackSignature StackSignature::operator+(const StackSignature& next) const { + StackSignature sig = *this; + sig += next; + return sig; +} + +StackFlow::StackFlow(Block* block) { + // Encapsulates the logic for treating the block and its children + // uniformly. The end of the block is treated as if it consumed values + // corresponding to the its result type and produced no values, which is why + // the block's result type is used as the params of its processed stack + // signature. + auto processBlock = [&block](auto process) { + // TODO: Once we support block parameters, set up the stack by calling + // `process` before iterating through the block. + for (auto* expr : block->list) { + process(expr, StackSignature(expr)); + } + bool unreachable = block->type == Type::unreachable; + Type params = unreachable ? Type::none : block->type; + process(block, StackSignature(params, Type::none, unreachable)); + }; + + // We need to make an initial pass through the block to figure out how many + // values each unreachable instruction produces. + std::unordered_map<Expression*, size_t> producedByUnreachable; + { + size_t stackSize = 0; + size_t produced = 0; + Expression* lastUnreachable = nullptr; + processBlock([&](Expression* expr, const StackSignature sig) { + // Consume params + if (sig.params.size() > stackSize) { + // We need more values than are available, so they must come from the + // last unreachable. + assert(lastUnreachable); + produced += sig.params.size() - stackSize; + stackSize = 0; + } else { + stackSize -= sig.params.size(); + } + + // Handle unreachable or produce results + if (sig.unreachable) { + if (lastUnreachable) { + producedByUnreachable[lastUnreachable] = produced; + produced = 0; + } + assert(produced == 0); + lastUnreachable = expr; + stackSize = 0; + } else { + stackSize += sig.results.size(); + } + }); + + // Finish record for final unreachable + if (lastUnreachable) { + producedByUnreachable[lastUnreachable] = produced; + } + } + + // Take another pass through the block, recording srcs and dests. + std::vector<Location> values; + Expression* lastUnreachable = nullptr; + processBlock([&](Expression* expr, const StackSignature sig) { + assert((sig.params.size() <= values.size() || lastUnreachable) && + "Block inputs not yet supported"); + + // Unreachable instructions consume all available values + size_t consumed = sig.unreachable + ? std::max(values.size(), sig.params.size()) + : sig.params.size(); + + // We previously calculated how many values unreachable instructions produce + size_t produced = + sig.unreachable ? producedByUnreachable[expr] : sig.results.size(); + + srcs[expr] = std::vector<Location>(consumed); + dests[expr] = std::vector<Location>(produced); + + // Figure out what kind of unreachable values we have + assert(sig.params.size() <= consumed); + size_t unreachableBeyondStack = 0; + size_t unreachableFromStack = 0; + if (consumed > values.size()) { + assert(consumed == sig.params.size()); + unreachableBeyondStack = consumed - values.size(); + } else if (consumed > sig.params.size()) { + assert(consumed == values.size()); + unreachableFromStack = consumed - sig.params.size(); + } + + // Consume values + for (Index i = 0; i < consumed; ++i) { + if (i < unreachableBeyondStack) { + // This value comes from the polymorphic stack of the last unreachable + // because the stack did not have enough values to satisfy this + // instruction. + assert(lastUnreachable); + assert(producedByUnreachable[lastUnreachable] >= + unreachableBeyondStack); + Index destIndex = + producedByUnreachable[lastUnreachable] - unreachableBeyondStack + i; + Type type = sig.params[i]; + srcs[expr][i] = {lastUnreachable, destIndex, type, true}; + dests[lastUnreachable][destIndex] = {expr, i, type, false}; + } else { + // A normal value from the value stack + bool unreachable = i < unreachableFromStack; + auto& src = values[values.size() + i - consumed]; + srcs[expr][i] = src; + dests[src.expr][src.index] = {expr, i, src.type, unreachable}; + } + } + + // Update available values + if (unreachableBeyondStack) { + producedByUnreachable[lastUnreachable] -= unreachableBeyondStack; + values.resize(0); + } else { + values.resize(values.size() - consumed); + } + + // Produce values + for (Index i = 0; i < sig.results.size(); ++i) { + values.push_back({expr, i, sig.results[i], false}); + } + + // Update the last unreachable instruction + if (sig.unreachable) { + assert(producedByUnreachable[lastUnreachable] == 0); + lastUnreachable = expr; + } + }); +} + +StackSignature StackFlow::getSignature(Expression* expr) { + auto exprSrcs = srcs.find(expr); + auto exprDests = dests.find(expr); + assert(exprSrcs != srcs.end() && exprDests != dests.end()); + std::vector<Type> params, results; + for (auto& src : exprSrcs->second) { + params.push_back(src.type); + } + for (auto& dest : exprDests->second) { + results.push_back(dest.type); + } + bool unreachable = expr->type == Type::unreachable; + return StackSignature(Type(params), Type(results), unreachable); +} + +} // namespace wasm diff --git a/src/ir/stack-utils.h b/src/ir/stack-utils.h new file mode 100644 index 000000000..89ec7d47e --- /dev/null +++ b/src/ir/stack-utils.h @@ -0,0 +1,196 @@ +/* + * Copyright 2020 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// stack-utils.h: Utilities for manipulating and analyzing stack machine code in +// the form of Poppy IR. +// +// Poppy IR represents stack machine code using normal Binaryen IR types by +// imposing the following constraints: +// +// 1. Function bodies and children of control flow (except If conditions) must +// be blocks. +// +// 2. Blocks may have any Expressions except for Pops as children. The sequence +// of instructions in a block follows the same validation rules as in +// WebAssembly. That means that any expression may have a concrete type, not +// just the final expression in the block. +// +// 3. All other children must be Pops, which are used to determine the input +// stack type of each instruction. Pops may not have `unreachable` type. +// +// 4. Only control flow structures and instructions that have polymorphic +// unreachable behavior in WebAssembly may have unreachable type. +// +// For example, the following Binaryen IR Function: +// +// (func $foo (result i32) +// (i32.add +// (i32.const 42) +// (i32.const 5) +// ) +// ) +// +// would look like this in Poppy IR: +// +// (func $foo (result i32) +// (block +// (i32.const 42) +// (i32.const 5) +// (i32.add +// (i32.pop) +// (i32.pop) +// ) +// ) +// ) +// +// Notice that the sequence of instructions in the block is now identical to the +// sequence of instructions in raw WebAssembly. +// + +#ifndef wasm_ir_stack_h +#define wasm_ir_stack_h + +#include "ir/properties.h" +#include "wasm-type.h" +#include "wasm.h" + +namespace wasm { + +namespace StackUtils { + +// Iterate through `block` and remove nops. +void removeNops(Block* block); + +} // namespace StackUtils + +// Stack signatures are like regular function signatures, but they are used to +// represent the stack parameters and results of arbitrary sequences of stacky +// instructions. They have to record whether they cover an unreachable +// instruction because their composition takes into account the polymorphic +// results of unreachable instructions. For example, the following instruction +// sequences both have params {i32, i32} and results {f32}, but only sequence B +// is unreachable: +// +// A: +// i32.add +// drop +// f32.const 42 +// +// B: +// i32.add +// unreachable +// f32.const 42 +// +// Notice that this distinction is important because sequence B can be the body +// of the blocks below but sequence A cannot. In other words, the stack +// signature of sequence B satisfies the signatures of these blocks, but the +// stack signature of sequence A does not. +// +// (block (param f64 i32 i32) (result f32) ... ) +// (block (param i32 i32) (result f64 f32) ... ) +// (block (param f64 i32 i32) (result f64 f32) ... ) +// +struct StackSignature { + Type params; + Type results; + bool unreachable; + + StackSignature() + : params(Type::none), results(Type::none), unreachable(false) {} + StackSignature(Type params, Type results, bool unreachable = false) + : params(params), results(results), unreachable(unreachable) {} + + StackSignature(const StackSignature&) = default; + StackSignature& operator=(const StackSignature&) = default; + + // Get the stack signature of `expr` + explicit StackSignature(Expression* expr); + + // Get the stack signature of the range of instructions [first, last). The + // sequence of instructions is assumed to be valid, i.e. their signatures + // compose. + template<class InputIt> + explicit StackSignature(InputIt first, InputIt last) + : params(Type::none), results(Type::none), unreachable(false) { + // TODO: It would be more efficient to build the signature directly and + // construct the params in reverse to avoid quadratic behavior. + while (first != last) { + *this += StackSignature(*first++); + } + } + + // Return `true` iff `next` composes after this stack signature. + bool composes(const StackSignature& next) const; + + // Whether a block whose contents have this stack signature could be typed + // with `sig`. + bool satisfies(Signature sig) const; + + // Compose stack signatures. Assumes they actually compose. + StackSignature& operator+=(const StackSignature& next); + StackSignature operator+(const StackSignature& next) const; + + bool operator==(const StackSignature& other) const { + return params == other.params && results == other.results && + unreachable == other.unreachable; + } +}; + +// Calculates stack machine data flow, associating the sources and destinations +// of all values in a block. +struct StackFlow { + // The destination (source) location at which a value of type `type` is + // consumed (produced), corresponding to the `index`th value consumed by + // (produced by) instruction `expr`. For destination locations, `unreachable` + // is true iff the corresponding value is consumed by the polymorphic behavior + // of an unreachable instruction rather than being used directly. For source + // locations, `unreachable` is true iff the corresponding value is produced by + // an unreachable instruction. For produced values that are not consumed + // within the block (TODO: also for consumed values that are not produced + // within the block), `expr` will be the enclosing block. + struct Location { + Expression* expr; + Index index; + Type type; + bool unreachable; + + bool operator==(const Location& other) const { + return expr == other.expr && index == other.index && type == other.type && + unreachable == other.unreachable; + } + }; + + using LocationMap = std::unordered_map<Expression*, std::vector<Location>>; + + // Maps each instruction to the set of source locations producing its inputs. + LocationMap srcs; + + // Maps each instruction to the set of output locations consuming its results. + LocationMap dests; + + // Gets the effective stack signature of `expr`, which must be a child of the + // block. If `expr` is unreachable, the returned signature will reflect the + // values consumed and produced by its polymorphic unreachable behavior. + StackSignature getSignature(Expression* expr); + + // Calculates `srcs` and `dests`. + StackFlow(Block* curr); +}; + +} // namespace wasm + +#endif // wasm_ir_stack_h diff --git a/src/wasm-type.h b/src/wasm-type.h index 8a9e58ec9..135c2e751 100644 --- a/src/wasm-type.h +++ b/src/wasm-type.h @@ -186,15 +186,39 @@ public: return index == other.index && parent == other.parent; } bool operator!=(const Iterator& other) const { return !(*this == other); } - void operator++() { index++; } + Iterator& operator++() { + ++index; + return *this; + } + Iterator& operator--() { + --index; + return *this; + } + Iterator operator++(int) { + auto it = *this; + index++; + return it; + } + Iterator operator--(int) { + auto it = *this; + index--; + return it; + } Iterator& operator+=(difference_type off) { index += off; return *this; } - const Iterator operator+(difference_type off) const { + Iterator operator+(difference_type off) const { return Iterator(*this) += off; } - difference_type operator-(const Iterator& other) { + Iterator& operator-=(difference_type off) { + index -= off; + return *this; + } + Iterator operator-(difference_type off) const { + return Iterator(*this) -= off; + } + difference_type operator-(const Iterator& other) const { assert(parent == other.parent); return index - other.index; } diff --git a/test/example/stack-utils.cpp b/test/example/stack-utils.cpp new file mode 100644 index 000000000..d35d30120 --- /dev/null +++ b/test/example/stack-utils.cpp @@ -0,0 +1,443 @@ +#include <cassert> +#include <iostream> + +#include "ir/stack-utils.h" +#include "literal.h" +#include "mixed_arena.h" +#include "wasm-builder.h" +#include "wasm-printing.h" +#include "wasm-type.h" +#include "wasm.h" + +using namespace wasm; + +MixedArena allocator; +Builder builder(allocator); + +void test_remove_nops() { + std::cout << ";; Test removeNops\n"; + auto* block = builder.makeBlock( + { + builder.makeNop(), + builder.makeConst(Literal(int32_t(0))), + builder.makeNop(), + builder.makeConst(Literal(int64_t(0))), + builder.makeNop(), + builder.makeNop(), + }, + {Type::i32, Type::i64}); + WasmPrinter::printExpression(block, std::cout); + std::cout << "\n"; + StackUtils::removeNops(block); + WasmPrinter::printExpression(block, std::cout); + std::cout << "\n"; +} + +void test_stack_signatures() { + std::cout << ";; Test stack signatures\n"; + // Typed block + auto* block = builder.makeBlock({builder.makeUnreachable()}, Type::f32); + assert(StackSignature(block) == + (StackSignature{Type::none, Type::f32, false})); + // Unreachable block + auto* unreachable = + builder.makeBlock({builder.makeUnreachable()}, Type::unreachable); + assert(StackSignature(unreachable) == + (StackSignature{Type::none, Type::none, true})); + { + // Typed loop + auto* loop = builder.makeLoop("loop", unreachable, Type::f32); + assert(StackSignature(loop) == + (StackSignature{Type::none, Type::f32, false})); + } + { + // Unreachable loop + auto* loop = builder.makeLoop("loop", unreachable, Type::unreachable); + assert(StackSignature(loop) == + (StackSignature{Type::none, Type::none, true})); + } + { + // If (no else) + auto* if_ = builder.makeIf( + builder.makePop(Type::i32), unreachable, nullptr, Type::none); + assert(StackSignature(if_) == + (StackSignature{Type::i32, Type::none, false})); + } + { + // If (with else) + auto* if_ = + builder.makeIf(builder.makePop(Type::i32), block, block, Type::f32); + assert(StackSignature(if_) == + (StackSignature{Type::i32, Type::f32, false})); + } + { + // Call + auto* call = + builder.makeCall("foo", + {builder.makePop(Type::i32), builder.makePop(Type::f32)}, + {Type::i64, Type::f64}); + assert( + StackSignature(call) == + (StackSignature{{Type::i32, Type::f32}, {Type::i64, Type::f64}, false})); + } + { + // Return Call + auto* call = + builder.makeCall("bar", + {builder.makePop(Type::i32), builder.makePop(Type::f32)}, + Type::unreachable, + true); + assert(StackSignature(call) == + (StackSignature{{Type::i32, Type::f32}, Type::none, true})); + } + { + // Return + auto* ret = builder.makeReturn(builder.makePop(Type::i32)); + assert(StackSignature(ret) == + (StackSignature{Type::i32, Type::none, true})); + } + { + // Multivalue return + auto* ret = builder.makeReturn(builder.makePop({Type::i32, Type::f32})); + assert(StackSignature(ret) == + (StackSignature{{Type::i32, Type::f32}, Type::none, true})); + } +} + +void test_signature_composition() { + std::cout << ";; Test stack signature composition\n"; + // No unreachables + { + StackSignature a{Type::none, {Type::f32, Type::i32}, false}; + StackSignature b{{Type::f32, Type::i32}, Type::none, false}; + assert(a.composes(b)); + assert(a + b == (StackSignature{Type::none, Type::none, false})); + } + { + StackSignature a{Type::none, Type::i32, false}; + StackSignature b{{Type::f32, Type::i32}, Type::none, false}; + assert(a.composes(b)); + assert(a + b == StackSignature(Type::f32, Type::none, false)); + } + { + StackSignature a{Type::none, {Type::f32, Type::i32}, false}; + StackSignature b{Type::i32, Type::none, false}; + assert(a.composes(b)); + assert(a + b == (StackSignature{Type::none, Type::f32, false})); + } + { + StackSignature a{Type::none, Type::f32, false}; + StackSignature b{{Type::f32, Type::i32}, Type::none, false}; + assert(!a.composes(b)); + } + { + StackSignature a{Type::none, {Type::f32, Type::i32}, false}; + StackSignature b{Type::f32, Type::none, false}; + assert(!a.composes(b)); + } + // First unreachable + { + StackSignature a{Type::none, {Type::f32, Type::i32}, true}; + StackSignature b{{Type::f32, Type::i32}, Type::none, false}; + assert(a.composes(b)); + assert(a + b == (StackSignature{Type::none, Type::none, true})); + } + { + StackSignature a{Type::none, Type::i32, true}; + StackSignature b{{Type::f32, Type::i32}, Type::none, false}; + assert(a.composes(b)); + assert(a + b == StackSignature(Type::none, Type::none, true)); + } + { + StackSignature a{Type::none, {Type::f32, Type::i32}, true}; + StackSignature b{Type::i32, Type::none, false}; + assert(a.composes(b)); + assert(a + b == (StackSignature{Type::none, Type::f32, true})); + } + { + StackSignature a{Type::none, Type::f32, true}; + StackSignature b{{Type::f32, Type::i32}, Type::none, false}; + assert(!a.composes(b)); + } + { + StackSignature a{Type::none, {Type::f32, Type::i32}, true}; + StackSignature b{Type::f32, Type::none, false}; + assert(!a.composes(b)); + } + // Second unreachable + { + StackSignature a{Type::none, {Type::f32, Type::i32}, false}; + StackSignature b{{Type::f32, Type::i32}, Type::none, true}; + assert(a.composes(b)); + assert(a + b == (StackSignature{Type::none, Type::none, true})); + } + { + StackSignature a{Type::none, Type::i32, false}; + StackSignature b{{Type::f32, Type::i32}, Type::none, true}; + assert(a.composes(b)); + assert(a + b == StackSignature(Type::f32, Type::none, true)); + } + { + StackSignature a{Type::none, {Type::f32, Type::i32}, false}; + StackSignature b{Type::i32, Type::none, true}; + assert(a.composes(b)); + assert(a + b == (StackSignature{Type::none, Type::none, true})); + } + { + StackSignature a{Type::none, Type::f32, false}; + StackSignature b{{Type::f32, Type::i32}, Type::none, true}; + assert(!a.composes(b)); + } + { + StackSignature a{Type::none, {Type::f32, Type::i32}, false}; + StackSignature b{Type::f32, Type::none, true}; + assert(!a.composes(b)); + } + // Both unreachable + { + StackSignature a{Type::none, {Type::f32, Type::i32}, true}; + StackSignature b{{Type::f32, Type::i32}, Type::none, true}; + assert(a.composes(b)); + assert(a + b == (StackSignature{Type::none, Type::none, true})); + } + { + StackSignature a{Type::none, Type::i32, true}; + StackSignature b{{Type::f32, Type::i32}, Type::none, true}; + assert(a.composes(b)); + assert(a + b == StackSignature(Type::none, Type::none, true)); + } + { + StackSignature a{Type::none, {Type::f32, Type::i32}, true}; + StackSignature b{Type::i32, Type::none, true}; + assert(a.composes(b)); + assert(a + b == (StackSignature{Type::none, Type::none, true})); + } + { + StackSignature a{Type::none, Type::f32, true}; + StackSignature b{{Type::f32, Type::i32}, Type::none, true}; + assert(!a.composes(b)); + } + { + StackSignature a{Type::none, {Type::f32, Type::i32}, true}; + StackSignature b{Type::f32, Type::none, true}; + assert(!a.composes(b)); + } +} + +void test_signature_satisfaction() { + std::cout << ";; Test stack signature satisfaction\n"; + // No unreachable + { + StackSignature a{Type::i32, Type::f32, false}; + Signature b(Type::i32, Type::f32); + assert(a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, false}; + Signature b({Type::i64, Type::i32}, {Type::i64, Type::f32}); + assert(a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, false}; + Signature b({Type::i64, Type::i32}, {Type::i64, Type::i64, Type::f32}); + assert(!a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, false}; + Signature b({Type::i64, Type::i32}, {Type::f64, Type::f32}); + assert(!a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, false}; + Signature b(Type::none, Type::f32); + assert(!a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, false}; + Signature b(Type::i32, Type::none); + assert(!a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, false}; + Signature b(Type::f32, Type::i32); + assert(!a.satisfies(b)); + } + // With unreachable + { + StackSignature a{Type::i32, Type::f32, true}; + Signature b(Type::i32, Type::f32); + assert(a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, true}; + Signature b({Type::i64, Type::i32}, {Type::i64, Type::f32}); + assert(a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, true}; + Signature b({Type::i64, Type::i32}, {Type::i64, Type::i64, Type::f32}); + assert(a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, true}; + Signature b({Type::i64, Type::i32}, {Type::f64, Type::f32}); + assert(a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, true}; + Signature b(Type::none, Type::f32); + assert(!a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, true}; + Signature b(Type::i32, Type::none); + assert(!a.satisfies(b)); + } + { + StackSignature a{Type::i32, Type::f32, true}; + Signature b(Type::f32, Type::i32); + assert(!a.satisfies(b)); + } +} + +void test_stack_flow() { + std::cout << ";; Test stack flow\n"; + { + // Simple case: + // foo + // │i32 + // bar + // │f32 + // end + auto* foo = builder.makeCall("foo", {}, Type::i32); + auto* bar = + builder.makeCall("bar", {builder.makePop(Type::i32)}, Type::f32); + auto* block = builder.makeBlock({foo, bar}, Type::f32); + StackFlow flow(block); + + auto fooSrcs = flow.srcs.find(foo); + assert(fooSrcs != flow.srcs.end()); + assert(fooSrcs->second.size() == 0); + + auto fooDests = flow.dests.find(foo); + assert(fooDests != flow.dests.end()); + assert(fooDests->second.size() == 1); + assert(fooDests->second[0] == + (StackFlow::Location{bar, 0, Type::i32, false})); + + auto barSrcs = flow.srcs.find(bar); + assert(barSrcs != flow.dests.end()); + assert(barSrcs->second.size() == 1); + assert(barSrcs->second[0] == + (StackFlow::Location{foo, 0, Type::i32, false})); + + auto barDests = flow.dests.find(bar); + assert(barDests != flow.dests.end()); + assert(barDests->second.size() == 1); + assert(barDests->second[0] == + (StackFlow::Location{block, 0, Type::f32, false})); + + auto blockSrcs = flow.srcs.find(block); + assert(blockSrcs != flow.srcs.end()); + assert(blockSrcs->second.size() == 1); + assert(blockSrcs->second[0] == + (StackFlow::Location{bar, 0, Type::f32, false})); + } + { + // Interesting case: + // foo + // ├─────┬─────┐ + // │i32 │f32 │i64 + // │ │ bar + // │ │ │f64 + // │ baz╶───┘ + // │ ├─────┐ + // ╵ ╵i64 │f32 + // ret╶─────────┘ + // ╷ ╷ ╷ + // │i64 │f64 │i32 + // │ quux╶──┘ + // end + auto* foo = builder.makeCall("foo", {}, {Type::i32, Type::f32, Type::i64}); + auto* bar = + builder.makeCall("bar", {builder.makePop(Type::i64)}, Type::f64); + auto* baz = + builder.makeCall("baz", + {builder.makePop(Type::f32), builder.makePop(Type::f64)}, + {Type::i64, Type::f32}); + auto* ret = builder.makeReturn(builder.makePop(Type::f32)); + auto* quux = + builder.makeCall("quux", + {builder.makePop(Type::f64), builder.makePop(Type::i32)}, + Type::none); + auto* block = builder.makeBlock({foo, bar, baz, ret, quux}, Type::i64); + StackFlow flow(block); + + assert(flow.srcs.find(foo)->second.size() == 0); + + const auto& fooDests = flow.dests[foo]; + assert(fooDests.size() == 3); + assert(fooDests[0] == (StackFlow::Location{ret, 0, Type::i32, true})); + assert(fooDests[1] == (StackFlow::Location{baz, 0, Type::f32, false})); + assert(fooDests[2] == (StackFlow::Location{bar, 0, Type::i64, false})); + + const auto& barSrcs = flow.srcs[bar]; + assert(barSrcs.size() == 1); + assert(barSrcs[0] == (StackFlow::Location{foo, 2, Type::i64, false})); + + const auto& barDests = flow.dests[bar]; + assert(barDests.size() == 1); + assert(barDests[0] == (StackFlow::Location{baz, 1, Type::f64, false})); + + const auto& bazSrcs = flow.srcs[baz]; + assert(bazSrcs.size() == 2); + assert(bazSrcs[0] == (StackFlow::Location{foo, 1, Type::f32, false})); + assert(bazSrcs[1] == (StackFlow::Location{bar, 0, Type::f64, false})); + + const auto& bazDests = flow.dests[baz]; + assert(bazDests.size() == 2); + assert(bazDests[0] == (StackFlow::Location{ret, 1, Type::i64, true})); + assert(bazDests[1] == (StackFlow::Location{ret, 2, Type::f32, false})); + + const auto& retSrcs = flow.srcs[ret]; + assert(retSrcs.size() == 3); + assert(retSrcs[0] == (StackFlow::Location{foo, 0, Type::i32, false})); + assert(retSrcs[1] == (StackFlow::Location{baz, 0, Type::i64, false})); + assert(retSrcs[2] == (StackFlow::Location{baz, 1, Type::f32, false})); + + const auto& retDests = flow.dests[ret]; + assert(retDests.size() == 3); + assert(retDests[0] == (StackFlow::Location{block, 0, Type::i64, false})); + assert(retDests[1] == (StackFlow::Location{quux, 0, Type::f64, false})); + assert(retDests[2] == (StackFlow::Location{quux, 1, Type::i32, false})); + + const auto& quuxSrcs = flow.srcs[quux]; + assert(quuxSrcs.size() == 2); + assert(quuxSrcs[0] == (StackFlow::Location{ret, 1, Type::f64, true})); + assert(quuxSrcs[1] == (StackFlow::Location{ret, 2, Type::i32, true})); + + const auto& quuxDests = flow.dests[quux]; + assert(quuxDests.size() == 0); + + const auto& blockSrcs = flow.srcs[block]; + assert(blockSrcs.size() == 1); + assert(blockSrcs[0] == (StackFlow::Location{ret, 0, Type::i64, true})); + + assert(flow.getSignature(foo) == StackSignature(foo)); + assert(flow.getSignature(bar) == StackSignature(bar)); + assert(flow.getSignature(baz) == StackSignature(baz)); + assert(flow.getSignature(ret) == + (StackSignature{{Type::i32, Type::i64, Type::f32}, + {Type::i64, Type::f64, Type::i32}, + true})); + assert(flow.getSignature(quux) == StackSignature(quux)); + } +} + +int main() { + test_remove_nops(); + test_stack_signatures(); + test_signature_composition(); + test_signature_satisfaction(); + test_stack_flow(); +} diff --git a/test/example/stack-utils.txt b/test/example/stack-utils.txt new file mode 100644 index 000000000..c278ca099 --- /dev/null +++ b/test/example/stack-utils.txt @@ -0,0 +1,17 @@ +;; Test removeNops +(block (result i32 i64) + (nop) + (i32.const 0) + (nop) + (i64.const 0) + (nop) + (nop) +) +(block (result i32 i64) + (i32.const 0) + (i64.const 0) +) +;; Test stack signatures +;; Test stack signature composition +;; Test stack signature satisfaction +;; Test stack flow |