From b0640a8b5a69b038f543f40925ad998c65029054 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 3 Feb 2021 18:09:06 -0800 Subject: [PATCH 1/8] StackSignature LUBs Add a utility for calculating the least upper bounds of two StackSignatures, taking into account polymorphic unreachable behavior. This will important in the finalization and validation of Poppy IR blocks, where a block is allowed to directly produce fewer values than the branches that target it carry if the difference can be made up for by polymorphism due to an unreachable instruction in the block. Also removes the Poppy IR documentation from stack-utils.h because that documentation is moved to the Stackify pass in #3541. --- src/ir/stack-utils.cpp | 29 ++++++++++++++++++++ src/ir/stack-utils.h | 53 +++--------------------------------- test/example/stack-utils.cpp | 49 +++++++++++++++++++++++++++++++++ test/example/stack-utils.txt | 1 + 4 files changed, 83 insertions(+), 49 deletions(-) diff --git a/src/ir/stack-utils.cpp b/src/ir/stack-utils.cpp index 1f70a661875..7fcfda777dd 100644 --- a/src/ir/stack-utils.cpp +++ b/src/ir/stack-utils.cpp @@ -160,6 +160,35 @@ StackSignature StackSignature::operator+(const StackSignature& next) const { return sig; } +StackSignature StackSignature::getLeastUpperBound(StackSignature a, + StackSignature b) { + // Assert an approximation of the conditions for having a LUB + assert(a.unreachable || b.unreachable || + (a.params.size() == b.params.size() && + a.results.size() == b.results.size())); + // Canonicalize so that the shorter types are on `a`. + if (a.params.size() > b.params.size()) { + std::swap(a.params, b.params); + } + if (a.results.size() > b.results.size()) { + std::swap(a.results, b.results); + } + std::vector params; + for (size_t i = 0, size = a.params.size(); i < size; ++i) { + params.push_back(Type::getLeastUpperBound(a.params[i], b.params[i])); + } + params.insert( + params.end(), b.params.begin() + a.params.size(), b.params.end()); + size_t resultsDiff = b.results.size() - a.results.size(); + std::vector results(b.results.begin(), b.results.begin() + resultsDiff); + for (size_t i = 0, size = a.results.size(); i < size; ++i) { + results.push_back( + Type::getLeastUpperBound(a.results[i], b.results[i + resultsDiff])); + } + return StackSignature{ + Type(params), Type(results), a.unreachable || b.unreachable}; +} + 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 diff --git a/src/ir/stack-utils.h b/src/ir/stack-utils.h index fc23b6080ff..55129a54444 100644 --- a/src/ir/stack-utils.h +++ b/src/ir/stack-utils.h @@ -16,55 +16,7 @@ // // 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. Blocks may -// be unreachable when they are not branch targets and when they have an -// unreachable child. Note that this means a block may be unreachable even -// if it would otherwise have a concrete type, unlike in Binaryen IR. -// -// 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. Also note that Poppy IR's -// validation rules are largely additional on top of the normal Binaryen IR -// validation rules, with the only exceptions being block body validation and -// block unreahchability rules. +// the form of Poppy IR. See src/passes/Stackify.cpp for Poppy IR documentation. // #ifndef wasm_ir_stack_h @@ -158,6 +110,9 @@ struct StackSignature { return params == other.params && results == other.results && unreachable == other.unreachable; } + + // Returns the LUB of `a` and `b`. Assumes that a LUB exists. + static StackSignature getLeastUpperBound(StackSignature a, StackSignature b); }; // Calculates stack machine data flow, associating the sources and destinations diff --git a/test/example/stack-utils.cpp b/test/example/stack-utils.cpp index 96bc6fb8565..1ff2c147acf 100644 --- a/test/example/stack-utils.cpp +++ b/test/example/stack-utils.cpp @@ -297,6 +297,54 @@ void test_signature_satisfaction() { } } +void test_signature_lub() { + std::cout << ";; Test stack signature lub\n"; + { + StackSignature a{Type::none, Type::none, false}; + StackSignature b{Type::none, Type::none, false}; + assert(StackSignature::getLeastUpperBound(a, b) == + (StackSignature{Type::none, Type::none, false})); + } + { + StackSignature a{Type::none, Type::none, true}; + StackSignature b{Type::none, Type::none, false}; + assert(StackSignature::getLeastUpperBound(a, b) == + (StackSignature{Type::none, Type::none, true})); + } + { + StackSignature a{Type::none, Type::none, false}; + StackSignature b{Type::none, Type::none, true}; + assert(StackSignature::getLeastUpperBound(a, b) == + (StackSignature{Type::none, Type::none, true})); + } + { + StackSignature a{Type::i32, Type::none, true}; + StackSignature b{Type::none, Type::i32, true}; + assert(StackSignature::getLeastUpperBound(a, b) == + (StackSignature{Type::i32, Type::i32, true})); + } + { + StackSignature a{Type::none, Type::i32, true}; + StackSignature b{Type::i32, Type::none, true}; + assert(StackSignature::getLeastUpperBound(a, b) == + (StackSignature{Type::i32, Type::i32, true})); + } + { + StackSignature a{Type::funcref, Type::externref, true}; + StackSignature b{Type::externref, Type::funcref, true}; + assert(StackSignature::getLeastUpperBound(a, b) == + (StackSignature{Type::anyref, Type::anyref, true})); + } + { + StackSignature a{{Type::funcref, Type::funcref}, Type::funcref, true}; + StackSignature b{Type::externref, {Type::externref, Type::externref}, true}; + assert(StackSignature::getLeastUpperBound(a, b) == + (StackSignature{{Type::anyref, Type::funcref}, + {Type::externref, Type::anyref}, + true})); + } +} + void test_stack_flow() { std::cout << ";; Test stack flow\n"; { @@ -436,5 +484,6 @@ int main() { test_stack_signatures(); test_signature_composition(); test_signature_satisfaction(); + test_signature_lub(); test_stack_flow(); } diff --git a/test/example/stack-utils.txt b/test/example/stack-utils.txt index c278ca099fd..42a77fcad69 100644 --- a/test/example/stack-utils.txt +++ b/test/example/stack-utils.txt @@ -14,4 +14,5 @@ ;; Test stack signatures ;; Test stack signature composition ;; Test stack signature satisfaction +;; Test stack signature lub ;; Test stack flow From 6164c592e9c21868d220869efc18dd7267585c1b Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 4 Feb 2021 10:38:28 -0800 Subject: [PATCH 2/8] Fix unreachability propagation and expand comment --- src/ir/stack-utils.cpp | 2 +- src/ir/stack-utils.h | 4 +++- test/example/stack-utils.cpp | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ir/stack-utils.cpp b/src/ir/stack-utils.cpp index 7fcfda777dd..08a4768c14c 100644 --- a/src/ir/stack-utils.cpp +++ b/src/ir/stack-utils.cpp @@ -186,7 +186,7 @@ StackSignature StackSignature::getLeastUpperBound(StackSignature a, Type::getLeastUpperBound(a.results[i], b.results[i + resultsDiff])); } return StackSignature{ - Type(params), Type(results), a.unreachable || b.unreachable}; + Type(params), Type(results), a.unreachable && b.unreachable}; } StackFlow::StackFlow(Block* block) { diff --git a/src/ir/stack-utils.h b/src/ir/stack-utils.h index 55129a54444..7d0c2e5cfb9 100644 --- a/src/ir/stack-utils.h +++ b/src/ir/stack-utils.h @@ -111,7 +111,9 @@ struct StackSignature { unreachable == other.unreachable; } - // Returns the LUB of `a` and `b`. Assumes that a LUB exists. + // Returns the LUB of `a` and `b`, i.e. the minimal StackSignature that could + // type block contents of either type `a` or type `b`. Assumes that a LUB + // exists. static StackSignature getLeastUpperBound(StackSignature a, StackSignature b); }; diff --git a/test/example/stack-utils.cpp b/test/example/stack-utils.cpp index 1ff2c147acf..e44100bc3a4 100644 --- a/test/example/stack-utils.cpp +++ b/test/example/stack-utils.cpp @@ -309,13 +309,13 @@ void test_signature_lub() { StackSignature a{Type::none, Type::none, true}; StackSignature b{Type::none, Type::none, false}; assert(StackSignature::getLeastUpperBound(a, b) == - (StackSignature{Type::none, Type::none, true})); + (StackSignature{Type::none, Type::none, false})); } { StackSignature a{Type::none, Type::none, false}; StackSignature b{Type::none, Type::none, true}; assert(StackSignature::getLeastUpperBound(a, b) == - (StackSignature{Type::none, Type::none, true})); + (StackSignature{Type::none, Type::none, false})); } { StackSignature a{Type::i32, Type::none, true}; From 1911ae16208b02917617c5142eac9f1df7bff0f0 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 4 Feb 2021 10:45:27 -0800 Subject: [PATCH 3/8] Improve asserts --- src/ir/stack-utils.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/ir/stack-utils.cpp b/src/ir/stack-utils.cpp index 08a4768c14c..e512c3c6f10 100644 --- a/src/ir/stack-utils.cpp +++ b/src/ir/stack-utils.cpp @@ -163,9 +163,12 @@ StackSignature StackSignature::operator+(const StackSignature& next) const { StackSignature StackSignature::getLeastUpperBound(StackSignature a, StackSignature b) { // Assert an approximation of the conditions for having a LUB - assert(a.unreachable || b.unreachable || - (a.params.size() == b.params.size() && - a.results.size() == b.results.size())); + assert((a.params.size() >= b.params.size() && + a.results.size() >= b.results.size()) || + a.unreachable); + assert((b.params.size() >= a.params.size() && + b.results.size() >= a.results.size()) || + b.unreachable); // Canonicalize so that the shorter types are on `a`. if (a.params.size() > b.params.size()) { std::swap(a.params, b.params); @@ -175,15 +178,19 @@ StackSignature StackSignature::getLeastUpperBound(StackSignature a, } std::vector params; for (size_t i = 0, size = a.params.size(); i < size; ++i) { - params.push_back(Type::getLeastUpperBound(a.params[i], b.params[i])); + Type lub = Type::getLeastUpperBound(a.params[i], b.params[i]); + assert(lub != Type::none); + params.push_back(lub); } params.insert( params.end(), b.params.begin() + a.params.size(), b.params.end()); size_t resultsDiff = b.results.size() - a.results.size(); std::vector results(b.results.begin(), b.results.begin() + resultsDiff); for (size_t i = 0, size = a.results.size(); i < size; ++i) { - results.push_back( - Type::getLeastUpperBound(a.results[i], b.results[i + resultsDiff])); + Type lub = + Type::getLeastUpperBound(a.results[i], b.results[i + resultsDiff]); + assert(lub != Type::none); + results.push_back(lub); } return StackSignature{ Type(params), Type(results), a.unreachable && b.unreachable}; From 26a629e0dab3fcc3b332035d800afb92c9712629 Mon Sep 17 00:00:00 2001 From: Thomas Lively <7121787+tlively@users.noreply.github.com> Date: Fri, 5 Feb 2021 10:38:38 -0800 Subject: [PATCH 4/8] Stackify => Poppify Co-authored-by: Heejin Ahn --- src/ir/stack-utils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ir/stack-utils.h b/src/ir/stack-utils.h index 7d0c2e5cfb9..e06224f6508 100644 --- a/src/ir/stack-utils.h +++ b/src/ir/stack-utils.h @@ -16,7 +16,7 @@ // // stack-utils.h: Utilities for manipulating and analyzing stack machine code in -// the form of Poppy IR. See src/passes/Stackify.cpp for Poppy IR documentation. +// the form of Poppy IR. See src/passes/Poppify.cpp for Poppy IR documentation. // #ifndef wasm_ir_stack_h From 0a5bde0544cdf91cebdf5eca077aff61a0eeb520 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 5 Feb 2021 18:52:57 -0800 Subject: [PATCH 5/8] Document and implement subtype relation --- src/ir/stack-utils.cpp | 177 +++++++++++++++++++++-------------- src/ir/stack-utils.h | 67 +++++++++++-- src/wasm/wasm-validator.cpp | 8 +- test/example/stack-utils.cpp | 177 ++++++++++++++++++++++++----------- test/example/stack-utils.txt | 2 +- 5 files changed, 294 insertions(+), 137 deletions(-) diff --git a/src/ir/stack-utils.cpp b/src/ir/stack-utils.cpp index e512c3c6f10..fca76f4d88e 100644 --- a/src/ir/stack-utils.cpp +++ b/src/ir/stack-utils.cpp @@ -83,48 +83,6 @@ bool StackSignature::composes(const StackSignature& next) const { }); } -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 stack(results.begin(), results.end()); @@ -160,38 +118,117 @@ StackSignature StackSignature::operator+(const StackSignature& next) const { return sig; } -StackSignature StackSignature::getLeastUpperBound(StackSignature a, - StackSignature b) { - // Assert an approximation of the conditions for having a LUB - assert((a.params.size() >= b.params.size() && - a.results.size() >= b.results.size()) || - a.unreachable); - assert((b.params.size() >= a.params.size() && - b.results.size() >= a.results.size()) || - b.unreachable); - // Canonicalize so that the shorter types are on `a`. - if (a.params.size() > b.params.size()) { - std::swap(a.params, b.params); +bool StackSignature::isSubType(StackSignature a, StackSignature b) { + if (a.params.size() > b.params.size() || + a.results.size() > b.results.size()) { + // `a` consumes or produces more values than `b` can provides or expects. + return false; } - if (a.results.size() > b.results.size()) { - std::swap(a.results, b.results); + if (b.unreachable && !a.unreachable) { + // Non-polymorphic sequences cannot be typed as being polymorphic. + return false; } - std::vector params; - for (size_t i = 0, size = a.params.size(); i < size; ++i) { - Type lub = Type::getLeastUpperBound(a.params[i], b.params[i]); - assert(lub != Type::none); - params.push_back(lub); + bool paramSuffixMatches = + std::equal(a.params.begin(), + a.params.end(), + b.params.end() - a.params.size(), + [](const Type& consumed, const Type& provided) { + return Type::isSubType(provided, consumed); + }); + if (!paramSuffixMatches) { + return false; } - params.insert( - params.end(), b.params.begin() + a.params.size(), b.params.end()); - size_t resultsDiff = b.results.size() - a.results.size(); - std::vector results(b.results.begin(), b.results.begin() + resultsDiff); - for (size_t i = 0, size = a.results.size(); i < size; ++i) { - Type lub = - Type::getLeastUpperBound(a.results[i], b.results[i + resultsDiff]); - assert(lub != Type::none); - results.push_back(lub); + bool resultSuffixMatches = + std::equal(a.results.begin(), + a.results.end(), + b.results.end() - a.results.size(), + [](const Type& produced, const Type& expected) { + return Type::isSubType(produced, expected); + }); + if (!resultSuffixMatches) { + return false; + } + if (a.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 + // compatible with the additional produced results. + return std::equal(b.params.begin(), + b.params.end() - a.params.size(), + b.results.begin(), + b.results.end() - a.results.size(), + [](const Type& provided, const Type& expected) { + return Type::isSubType(provided, expected); + }); +} + +bool StackSignature::haveLeastUpperBound(StackSignature a, StackSignature b) { + // Params and results may only be shorter on unreachable stack signatures. + if (!a.unreachable && (a.params.size() < b.params.size() || + a.results.size() < b.results.size())) { + return false; + } + if (!b.unreachable && (b.params.size() < a.params.size() || + b.results.size() < a.results.size())) { + return false; + } + + auto valsCompatible = [](auto as, auto bs, auto compatible) -> bool { + // Canonicalize so the as are shorter and any unshared prefix is on bs. + if (bs.size() < as.size()) { + std::swap(as, bs); + } + // Check that shared values after the unshared prefix have are compatible. + size_t diff = bs.size() - as.size(); + for (size_t i = 0, shared = as.size(); i < shared; ++i) { + if (!compatible(as[i], bs[i + diff])) { + return false; + } + } + return true; + }; + + bool paramsOk = valsCompatible(a.params, b.params, [](Type a, Type b) { + assert(a == b && "TODO: calculate greatest lower bound to handle " + "contravariance correctly"); + return true; + }); + bool resultsOk = valsCompatible(a.results, b.results, [](Type a, Type b) { + return Type::getLeastUpperBound(a, b) != Type::none; + }); + return paramsOk && resultsOk; +} + +StackSignature StackSignature::getLeastUpperBound(StackSignature a, + StackSignature b) { + assert(haveLeastUpperBound(a, b)); + + auto combineVals = [](auto as, auto bs, auto combine) -> std::vector { + // Canonicalize so the as are shorter and any unshared prefix is on bs. + if (bs.size() < as.size()) { + std::swap(as, bs); + } + // Combine shared values after the unshared prefix. + size_t diff = bs.size() - as.size(); + std::vector vals(bs.begin(), bs.begin() + diff); + for (size_t i = 0, shared = as.size(); i < shared; ++i) { + vals.push_back(combine(as[i], bs[i + diff])); + } + return vals; + }; + + auto params = combineVals(a.params, b.params, [&](Type a, Type b) { + assert(a == b && "TODO: calculate greatest lower bound to handle " + "contravariance correctly"); + return a; + }); + + auto results = combineVals(a.results, b.results, [&](Type a, Type b) { + return Type::getLeastUpperBound(a, b); + }); + return StackSignature{ Type(params), Type(results), a.unreachable && b.unreachable}; } diff --git a/src/ir/stack-utils.h b/src/ir/stack-utils.h index e06224f6508..67bd151b074 100644 --- a/src/ir/stack-utils.h +++ b/src/ir/stack-utils.h @@ -73,7 +73,7 @@ struct StackSignature { StackSignature() : params(Type::none), results(Type::none), unreachable(false) {} - StackSignature(Type params, Type results, bool unreachable = false) + StackSignature(Type params, Type results, bool unreachable) : params(params), results(results), unreachable(unreachable) {} StackSignature(const StackSignature&) = default; @@ -98,10 +98,6 @@ struct StackSignature { // 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; @@ -111,9 +107,64 @@ struct StackSignature { unreachable == other.unreachable; } - // Returns the LUB of `a` and `b`, i.e. the minimal StackSignature that could - // type block contents of either type `a` or type `b`. Assumes that a LUB - // exists. + // Whether a block whose contents have stack signature `a` could be typed with + // stack signature `b`, i.e. whether it could be used in a context that + // expects signature `b`. Formally: + // + // [t1*] -> [t2*] <: [s1* t1'*] -> [s2* t2'*] iff + // + // - t1'_i <: t1_i + // - t2_i <: t2'_i + // - s1_i <: s2_i + // + // [t1*] -> [t2*] {u} <: [s1* t1'*] -> [s2* t2'*] {u?} iff + // + // - [t1*] -> [t2*] <: [t1'*] -> [t2'*] + // + // As an example of the first rule, consider this instruction sequence: + // + // ref.as_func + // drop + // i32.add + // + // The most specific type you could give this sequence is [i32, i32, anyref] + // -> [i32]. But it could also be used in a context that expects [i32, i32, + // funcref] -> [i32] because ref.as_func can accept funcref or any other + // subtype of anyref. That's where the contravariance comes from. This + // instruction sequence could also be used anywhere that expects [f32, i32, + // i32, anyref] -> [f32, anyref] because the f32 simply stays on the stack + // throughout the sequence. That's where the the prefix extension comes from. + // + // For the second rule, consider this sequence: + // + // ref.as_func + // drop + // i32.add + // unreachable + // i32.const 0 + // + // This instruction sequence has the specific type [i32, i32, anyref] -> [i32] + // {u}. It can be used in any situation the previous block can be used in, but + // can additionally be used in contexts that expect something like [f32, i32, + // i32, anyref] -> [v128, i32]. Because of the unreachable polymorphism, the + // additional prefixes on the params and results do not need to match. + // + // Note that a reachable stack signature (without a {u}) is never a subtype of + // any unreachable stack signature (with a {u}). This makes sense because a + // sequence of instructions that has no polymorphic unreachable behavior + // cannot be given a type that says it does have polymorphic unreachable + // behavior. + // + // Also, [] -> [] {u} is the bottom type here; it is a subtype of every other + // stack signature. This corresponds to (unreachable) being able to be given + // any stack signature. + static bool isSubType(StackSignature a, StackSignature b); + + // Returns true iff `a` and `b` have a LUB, i.e. a minimal StackSignature that + // could type block contents of either type `a` or type `b`. + static bool haveLeastUpperBound(StackSignature a, StackSignature b); + + // Returns the LUB of `a` and `b`. Assumes that the LUB exists. static StackSignature getLeastUpperBound(StackSignature a, StackSignature b); }; diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index 6a1b724768a..899f8e6839e 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -633,9 +633,11 @@ void FunctionValidator::validatePoppyBlockElements(Block* curr) { curr, "unreachable block should have unreachable element"); } else { - if (!shouldBeTrue(blockSig.satisfies(Signature(Type::none, curr->type)), - curr, - "block contents should satisfy block type") && + if (!shouldBeTrue( + StackSignature::isSubType( + blockSig, StackSignature(Type::none, curr->type, false)), + curr, + "block contents should satisfy block type") && !info.quiet) { getStream() << "contents: " << blockSig.results << (blockSig.unreachable ? " [unreachable]" : "") << "\n" diff --git a/test/example/stack-utils.cpp b/test/example/stack-utils.cpp index e44100bc3a4..753e859a772 100644 --- a/test/example/stack-utils.cpp +++ b/test/example/stack-utils.cpp @@ -221,79 +221,102 @@ void test_signature_composition() { } } -void test_signature_satisfaction() { - std::cout << ";; Test stack signature satisfaction\n"; - // No unreachable +void test_signature_subtype() { + std::cout << ";; Test stack signature subtyping\n"; + // Differences in unreachability only { - StackSignature a{Type::i32, Type::f32, false}; - Signature b(Type::i32, Type::f32); - assert(a.satisfies(b)); + StackSignature a(Type::none, Type::none, true); + StackSignature b(Type::none, Type::none, false); + assert(StackSignature::isSubType(a, b)); + assert(!StackSignature::isSubType(b, a)); } + // Covariance of results { - StackSignature a{Type::i32, Type::f32, false}; - Signature b({Type::i64, Type::i32}, {Type::i64, Type::f32}); - assert(a.satisfies(b)); + StackSignature a(Type::none, Type::funcref, false); + StackSignature b(Type::none, Type::anyref, false); + assert(StackSignature::isSubType(a, b)); + assert(!StackSignature::isSubType(b, a)); } + // Contravariance of params { - 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::anyref, Type::none, false); + StackSignature b(Type::funcref, Type::none, false); + // assert(StackSignature::isSubType(a, b)); + // assert(!StackSignature::isSubType(b, a)); } + // First not unreachable { - 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); + StackSignature b(Type::i32, Type::f32, false); + assert(StackSignature::isSubType(a, 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); + StackSignature b({Type::i64, Type::i32}, {Type::i64, Type::f32}, false); + assert(StackSignature::isSubType(a, 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); + StackSignature b( + {Type::i64, Type::i32}, {Type::i64, Type::i64, Type::f32}, false); + assert(!StackSignature::isSubType(a, b)); } { - StackSignature a{Type::i32, Type::f32, false}; - Signature b(Type::f32, Type::i32); - assert(!a.satisfies(b)); + StackSignature a(Type::i32, Type::f32, false); + StackSignature b({Type::i64, Type::i32}, {Type::f64, Type::f32}, false); + assert(!StackSignature::isSubType(a, 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, false); + StackSignature b(Type::none, Type::f32, false); + assert(!StackSignature::isSubType(a, 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, false); + StackSignature b(Type::i32, Type::none, false); + assert(!StackSignature::isSubType(a, 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, false); + StackSignature b(Type::f32, Type::i32, false); + assert(!StackSignature::isSubType(a, b)); + } + // First unreachable + { + StackSignature a(Type::i32, Type::f32, true); + StackSignature b(Type::i32, Type::f32, false); + assert(StackSignature::isSubType(a, 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); + StackSignature b({Type::i64, Type::i32}, {Type::i64, Type::f32}, false); + assert(StackSignature::isSubType(a, 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); + StackSignature b( + {Type::i64, Type::i32}, {Type::i64, Type::i64, Type::f32}, false); + assert(StackSignature::isSubType(a, 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); + StackSignature b({Type::i64, Type::i32}, {Type::f64, Type::f32}, false); + assert(StackSignature::isSubType(a, b)); } { - StackSignature a{Type::i32, Type::f32, true}; - Signature b(Type::f32, Type::i32); - assert(!a.satisfies(b)); + StackSignature a(Type::i32, Type::f32, true); + StackSignature b(Type::none, Type::f32, false); + assert(!StackSignature::isSubType(a, b)); + } + { + StackSignature a(Type::i32, Type::f32, true); + StackSignature b(Type::i32, Type::none, false); + assert(!StackSignature::isSubType(a, b)); + } + { + StackSignature a(Type::i32, Type::f32, true); + StackSignature b(Type::f32, Type::i32, false); + assert(!StackSignature::isSubType(a, b)); } } @@ -302,46 +325,90 @@ void test_signature_lub() { { StackSignature a{Type::none, Type::none, false}; StackSignature b{Type::none, Type::none, false}; + assert(StackSignature::haveLeastUpperBound(a, b)); assert(StackSignature::getLeastUpperBound(a, b) == (StackSignature{Type::none, Type::none, false})); } { StackSignature a{Type::none, Type::none, true}; StackSignature b{Type::none, Type::none, false}; + assert(StackSignature::haveLeastUpperBound(a, b)); assert(StackSignature::getLeastUpperBound(a, b) == (StackSignature{Type::none, Type::none, false})); } { StackSignature a{Type::none, Type::none, false}; StackSignature b{Type::none, Type::none, true}; + assert(StackSignature::haveLeastUpperBound(a, b)); assert(StackSignature::getLeastUpperBound(a, b) == (StackSignature{Type::none, Type::none, false})); } { StackSignature a{Type::i32, Type::none, true}; StackSignature b{Type::none, Type::i32, true}; + assert(StackSignature::haveLeastUpperBound(a, b)); assert(StackSignature::getLeastUpperBound(a, b) == (StackSignature{Type::i32, Type::i32, true})); } { StackSignature a{Type::none, Type::i32, true}; StackSignature b{Type::i32, Type::none, true}; + assert(StackSignature::haveLeastUpperBound(a, b)); assert(StackSignature::getLeastUpperBound(a, b) == (StackSignature{Type::i32, Type::i32, true})); } { - StackSignature a{Type::funcref, Type::externref, true}; - StackSignature b{Type::externref, Type::funcref, true}; + StackSignature a{Type::none, Type::externref, true}; + StackSignature b{Type::none, Type::funcref, true}; + assert(StackSignature::haveLeastUpperBound(a, b)); assert(StackSignature::getLeastUpperBound(a, b) == - (StackSignature{Type::anyref, Type::anyref, true})); + (StackSignature{Type::none, Type::anyref, true})); + } + { + StackSignature a{Type::anyref, Type::none, true}; + StackSignature b{Type::funcref, Type::none, true}; + // assert(StackSignature::haveLeastUpperBound(a, b)); + // assert(StackSignature::getLeastUpperBound(a, b) == + // (StackSignature{Type::funcref, Type::none, true})); } { - StackSignature a{{Type::funcref, Type::funcref}, Type::funcref, true}; - StackSignature b{Type::externref, {Type::externref, Type::externref}, true}; + StackSignature a{{Type::i32, Type::funcref}, Type::funcref, true}; + StackSignature b{Type::funcref, {Type::f32, Type::externref}, true}; + assert(StackSignature::haveLeastUpperBound(a, b)); assert(StackSignature::getLeastUpperBound(a, b) == - (StackSignature{{Type::anyref, Type::funcref}, - {Type::externref, Type::anyref}, - true})); + (StackSignature{ + {Type::i32, Type::funcref}, {Type::f32, Type::anyref}, true})); + } + // No LUB + { + StackSignature a(Type::none, Type::i32, false); + StackSignature b(Type::none, Type::f32, false); + assert(!StackSignature::haveLeastUpperBound(a, b)); + } + { + StackSignature a(Type::none, Type::i32, true); + StackSignature b(Type::none, Type::f32, true); + assert(!StackSignature::haveLeastUpperBound(a, b)); + } + { + StackSignature a(Type::i32, Type::none, false); + StackSignature b(Type::f32, Type::none, false); + // assert(!StackSignature::haveLeastUpperBound(a, b)); + } + { + StackSignature a(Type::i32, Type::none, true); + StackSignature b(Type::f32, Type::none, true); + // assert(!StackSignature::haveLeastUpperBound(a, b)); + } + { + StackSignature a(Type::none, Type::none, false); + StackSignature b(Type::none, Type::i32, true); + assert(!StackSignature::haveLeastUpperBound(a, b)); + } + { + StackSignature a(Type::none, Type::none, false); + StackSignature b(Type::i32, Type::none, true); + assert(!StackSignature::haveLeastUpperBound(a, b)); } } @@ -483,7 +550,7 @@ int main() { test_remove_nops(); test_stack_signatures(); test_signature_composition(); - test_signature_satisfaction(); + test_signature_subtype(); test_signature_lub(); test_stack_flow(); } diff --git a/test/example/stack-utils.txt b/test/example/stack-utils.txt index 42a77fcad69..d24119aab26 100644 --- a/test/example/stack-utils.txt +++ b/test/example/stack-utils.txt @@ -13,6 +13,6 @@ ) ;; Test stack signatures ;; Test stack signature composition -;; Test stack signature satisfaction +;; Test stack signature subtyping ;; Test stack signature lub ;; Test stack flow From d72ba69e1f586527ffbd238dec477771ac760881 Mon Sep 17 00:00:00 2001 From: Thomas Lively <7121787+tlively@users.noreply.github.com> Date: Tue, 9 Feb 2021 13:05:37 -0800 Subject: [PATCH 6/8] anyref => i32 in docs Co-authored-by: Heejin Ahn --- src/ir/stack-utils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ir/stack-utils.h b/src/ir/stack-utils.h index 67bd151b074..4db857736f3 100644 --- a/src/ir/stack-utils.h +++ b/src/ir/stack-utils.h @@ -132,7 +132,7 @@ struct StackSignature { // funcref] -> [i32] because ref.as_func can accept funcref or any other // subtype of anyref. That's where the contravariance comes from. This // instruction sequence could also be used anywhere that expects [f32, i32, - // i32, anyref] -> [f32, anyref] because the f32 simply stays on the stack + // i32, anyref] -> [f32, i32] because the f32 simply stays on the stack // throughout the sequence. That's where the the prefix extension comes from. // // For the second rule, consider this sequence: From abfe00728ec6a1884d53f74dbd6a08deca7bb128 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 9 Feb 2021 13:30:20 -0800 Subject: [PATCH 7/8] Improve doc comment and enable contravariance tests --- src/ir/stack-utils.h | 14 ++++++++++---- test/example/stack-utils.cpp | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ir/stack-utils.h b/src/ir/stack-utils.h index 4db857736f3..4188402638f 100644 --- a/src/ir/stack-utils.h +++ b/src/ir/stack-utils.h @@ -109,18 +109,24 @@ struct StackSignature { // Whether a block whose contents have stack signature `a` could be typed with // stack signature `b`, i.e. whether it could be used in a context that - // expects signature `b`. Formally: + // expects signature `b`. Formally, where `a` is the LHS and `b` the RHS: // // [t1*] -> [t2*] <: [s1* t1'*] -> [s2* t2'*] iff // - // - t1'_i <: t1_i - // - t2_i <: t2'_i - // - s1_i <: s2_i + // - t1' <: t1 + // - t2 <: t2' + // - s1 <: s2 + // + // Note that neither signature is unreachable in this rule and that the + // cardinalities of t1* and t1'*, t2* and t2'*, and s1* and s2* must match. // // [t1*] -> [t2*] {u} <: [s1* t1'*] -> [s2* t2'*] {u?} iff // // - [t1*] -> [t2*] <: [t1'*] -> [t2'*] // + // Note that s1* and s2* can have different cardinalities and arbitrary types + // in this rule. + // // As an example of the first rule, consider this instruction sequence: // // ref.as_func diff --git a/test/example/stack-utils.cpp b/test/example/stack-utils.cpp index 753e859a772..5bae6e9ac80 100644 --- a/test/example/stack-utils.cpp +++ b/test/example/stack-utils.cpp @@ -241,8 +241,8 @@ void test_signature_subtype() { { StackSignature a(Type::anyref, Type::none, false); StackSignature b(Type::funcref, Type::none, false); - // assert(StackSignature::isSubType(a, b)); - // assert(!StackSignature::isSubType(b, a)); + assert(StackSignature::isSubType(a, b)); + assert(!StackSignature::isSubType(b, a)); } // First not unreachable { From de06f9e009d03944411c3e4c5472b901a961a6a0 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 10 Feb 2021 16:42:11 -0800 Subject: [PATCH 8/8] Fix extension condition --- src/ir/stack-utils.cpp | 33 ++++++++++++++++++++++++++------- test/example/stack-utils.cpp | 15 +++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/ir/stack-utils.cpp b/src/ir/stack-utils.cpp index fca76f4d88e..05b7f6de644 100644 --- a/src/ir/stack-utils.cpp +++ b/src/ir/stack-utils.cpp @@ -165,13 +165,32 @@ bool StackSignature::isSubType(StackSignature a, StackSignature b) { } bool StackSignature::haveLeastUpperBound(StackSignature a, StackSignature b) { - // Params and results may only be shorter on unreachable stack signatures. - if (!a.unreachable && (a.params.size() < b.params.size() || - a.results.size() < b.results.size())) { - return false; - } - if (!b.unreachable && (b.params.size() < a.params.size() || - b.results.size() < a.results.size())) { + // If a signature is unreachable, the LUB could extend its params and results + // arbitrarily. Otherwise, the LUB must extend its params and results + // uniformly so that each additional param is a subtype of the corresponding + // additional result. + auto extensionCompatible = [](auto self, auto other) -> bool { + if (self.unreachable) { + return true; + } + // If no extension, then no problem. + if (self.params.size() >= other.params.size() && + self.results.size() >= other.results.size()) { + return true; + } + auto extSize = other.params.size() - self.params.size(); + if (extSize != other.results.size() - self.results.size()) { + return false; + } + return std::equal(other.params.begin(), + other.params.begin() + extSize, + other.results.begin(), + other.results.begin() + extSize, + [](const Type& param, const Type& result) { + return Type::isSubType(param, result); + }); + }; + if (!extensionCompatible(a, b) || !extensionCompatible(b, a)) { return false; } diff --git a/test/example/stack-utils.cpp b/test/example/stack-utils.cpp index 5bae6e9ac80..63f0d2c66b3 100644 --- a/test/example/stack-utils.cpp +++ b/test/example/stack-utils.cpp @@ -410,6 +410,21 @@ void test_signature_lub() { StackSignature b(Type::i32, Type::none, true); assert(!StackSignature::haveLeastUpperBound(a, b)); } + { + StackSignature a{Type::none, Type::i32, false}; + StackSignature b{Type::i32, Type::none, false}; + assert(!StackSignature::haveLeastUpperBound(a, b)); + } + { + StackSignature a{Type::none, Type::i32, true}; + StackSignature b{Type::i32, Type::none, false}; + assert(!StackSignature::haveLeastUpperBound(a, b)); + } + { + StackSignature a{Type::none, Type::i32, false}; + StackSignature b{Type::i32, Type::none, true}; + assert(!StackSignature::haveLeastUpperBound(a, b)); + } } void test_stack_flow() {