| /* | |
| * Copyright 2026 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 <optional> | |
| #include "ir/constraint.h" | |
| #include "ir/find_all.h" | |
| #include "ir/properties.h" | |
| #include "wasm.h" | |
| namespace wasm::constraint { | |
| namespace { | |
| std::optional<SpansU2> | |
| getSpansInternal(const Constraint& c, std::optional<Type> type, bool exact) { | |
| using namespace Abstract; | |
| auto* cc = std::get_if<Literal>(&c.term); | |
| if (cc) { | |
| // If passed in, the type must be right. | |
| assert(!type || *type == cc->type); | |
| type = cc->type; | |
| } | |
| if (type && !type->isInteger()) { | |
| // References etc. do not convert to spans. | |
| return {}; | |
| } | |
| // Maximum values, as represented as uint64_t's. We generate spans on unsigned | |
| // values, converting signed ones to their unsigned representations. | |
| uint64_t maxUnsigned = type && *type == Type::i32 | |
| ? std::numeric_limits<uint32_t>::max() | |
| : std::numeric_limits<uint64_t>::max(); | |
| uint64_t maxSigned = type && *type == Type::i32 | |
| ? std::numeric_limits<int32_t>::max() | |
| : std::numeric_limits<int64_t>::max(); | |
| uint64_t minSigned = maxSigned + 1; | |
| if (!cc) { | |
| // Not comparing to a constant, so we can't infer anything exact, but might | |
| // if we just need something we can prove, and if we know the type. | |
| if (exact || !type) { | |
| return {}; | |
| } | |
| switch (c.op) { | |
| // x < y, i.e., x is less than *something*, proves x != MAX_INT. | |
| case LtS: | |
| // In the signed case, this is a pair of spans: all to the left and all | |
| // to the right of MAX_INT. | |
| return SpansU2{{0, maxSigned - 1}, {maxSigned + 1, maxUnsigned}}; | |
| case LtU: | |
| return SpansU2{{0, maxUnsigned - 1}}; | |
| // Similarly, x > y proves x != MIN_INT. | |
| case GtS: | |
| return SpansU2{{0, minSigned - 1}, {minSigned + 1, maxUnsigned}}; | |
| case GtU: | |
| return SpansU2{{1, maxUnsigned}}; | |
| default: { | |
| } | |
| } | |
| return {}; | |
| } | |
| auto x = cc->getUnsigned(); | |
| switch (c.op) { | |
| case Eq: | |
| return SpansU2{{x, x}}; | |
| case Ne: | |
| if (x == 0) { | |
| return SpansU2{{1, maxUnsigned}}; | |
| } | |
| if (x == maxUnsigned) { | |
| return SpansU2{{0, maxUnsigned - 1}}; | |
| } | |
| return SpansU2{{0, x - 1}, {x + 1, maxUnsigned}}; | |
| case LtS: | |
| if (x == minSigned) { | |
| // Less than the lowest possible number is an empty span. | |
| return SpansU2{}; | |
| } | |
| if (x > maxSigned) { | |
| // A negative number, so just a single span. | |
| return SpansU2{{maxSigned + 1, x - 1}}; | |
| } | |
| if (x == 0) { | |
| // All negative numbers are possible. | |
| return SpansU2{{maxSigned + 1, maxUnsigned}}; | |
| } | |
| // A positive number, so all negative ones are possible, and some | |
| // positive. | |
| return SpansU2{{0, x - 1}, {maxSigned + 1, maxUnsigned}}; | |
| case LtU: | |
| if (x == 0) { | |
| // Less than the lowest possible number is an empty span. | |
| return SpansU2{}; | |
| } | |
| return SpansU2{{0, x - 1}}; | |
| case LeS: | |
| if (x > maxSigned) { | |
| // A negative number, so just a single span. | |
| return SpansU2{{maxSigned + 1, x}}; | |
| } | |
| if (x == maxSigned) { | |
| // All numbers are possible. | |
| return SpansU2{{0, maxUnsigned}}; | |
| } | |
| // A non-negative number, so all negative ones are possible, and some | |
| // positive. | |
| return SpansU2{{0, x}, {maxSigned + 1, maxUnsigned}}; | |
| case LeU: | |
| return SpansU2{{0, x}}; | |
| case GtS: | |
| if (x == maxSigned) { | |
| // Greater than the highest possible number is an empty span. | |
| return SpansU2{}; | |
| } | |
| if (x <= maxSigned) { | |
| // A non-negative number, so just a single span. | |
| return SpansU2{{x + 1, maxSigned}}; | |
| } | |
| if (x == maxUnsigned) { | |
| // GtS negative one, so 0 and above. | |
| return SpansU2{{0, maxSigned}}; | |
| } | |
| // A negative number, so all positive ones are possible, and some | |
| // negative. | |
| return SpansU2{{0, maxSigned}, {x + 1, maxUnsigned}}; | |
| case GtU: | |
| if (x == maxUnsigned) { | |
| // Greater than the highest possible number is an empty span. | |
| return SpansU2{}; | |
| } | |
| return SpansU2{{x + 1, maxUnsigned}}; | |
| case GeS: | |
| if (x == minSigned) { | |
| // All numbers are possible. | |
| return SpansU2{{0, maxUnsigned}}; | |
| } | |
| if (x <= maxSigned) { | |
| // A non-negative number, so just a single span. | |
| return SpansU2{{x, maxSigned}}; | |
| } | |
| // A negative number, so all positive ones are possible, and some | |
| // negative. | |
| return SpansU2{{0, maxSigned}, {x, maxUnsigned}}; | |
| case GeU: | |
| return SpansU2{{x, maxUnsigned}}; | |
| default: { | |
| } | |
| } | |
| return {}; | |
| } | |
| } // anonymous namespace | |
| std::optional<SpansU2> Constraint::getSpans(std::optional<Type> type) const { | |
| return getSpansInternal(*this, type, true); | |
| } | |
| std::optional<SpansU2> | |
| Constraint::getProvenSpans(std::optional<Type> type) const { | |
| return getSpansInternal(*this, type, false); | |
| } | |
| namespace { | |
| Result TrueFalse(bool x) { return x ? True : False; } | |
| Result TrueFalse(Literal x) { return TrueFalse(x.getUnsigned()); } | |
| // Evaluate whether a => b, where a and b are operations on constants. | |
| Result provesConstantPair(Abstract::Op aOp, | |
| const Literal& aConstant, | |
| Abstract::Op bOp, | |
| const Literal& bConstant, | |
| bool recursing = false) { | |
| using namespace Abstract; | |
| // a == A =?=> a op B. Simply apply A to the operation against B. | |
| if (aOp == Eq) { | |
| switch (bOp) { | |
| case Eq: | |
| return TrueFalse(aConstant.eq(bConstant)); | |
| case Ne: | |
| return TrueFalse(aConstant.ne(bConstant)); | |
| case LtS: | |
| return TrueFalse(aConstant.ltS(bConstant)); | |
| case LeS: | |
| return TrueFalse(aConstant.leS(bConstant)); | |
| case GtS: | |
| return TrueFalse(aConstant.gtS(bConstant)); | |
| case GeS: | |
| return TrueFalse(aConstant.geS(bConstant)); | |
| case LtU: | |
| return TrueFalse(aConstant.ltU(bConstant)); | |
| case LeU: | |
| return TrueFalse(aConstant.leU(bConstant)); | |
| case GtU: | |
| return TrueFalse(aConstant.gtU(bConstant)); | |
| case GeU: | |
| return TrueFalse(aConstant.geU(bConstant)); | |
| default: { | |
| } | |
| } | |
| } | |
| // a != A =?=> a == B. False if A = B, else unknown. | |
| if (aOp == Ne && bOp == Eq) { | |
| if (aConstant.eq(bConstant).getInteger()) { | |
| return False; | |
| } | |
| } | |
| // a != A =?=> a != B. True if A = B, else unknown. | |
| if (aOp == Ne && bOp == Ne) { | |
| if (aConstant.eq(bConstant).getInteger()) { | |
| return True; | |
| } | |
| } | |
| if (!recursing) { | |
| // The flipped operation may tell us something: y ==> !x implies | |
| // x ==> y is false (because if not, then x would prove y, and y would | |
| // prove !x, a contradiction). | |
| if (provesConstantPair(bOp, bConstant, aOp, aConstant, true) == False) { | |
| return False; | |
| } | |
| } | |
| // TODO: handle all the rest of >, >=, <, and <= | |
| return Unknown; | |
| } | |
| // Evaluate whether a => b, where a and b are operations on identical terms. | |
| Result provesTermEqualPair(Abstract::Op aOp, Abstract::Op bOp) { | |
| using namespace Abstract; | |
| // Trivial cases where aOp == bOp or aOp == !bOp are taken care of elsewhere. | |
| assert(aOp != bOp && aOp != Abstract::negateRelational(bOp)); | |
| switch (aOp) { | |
| case Eq: | |
| // == proves >= etc. true, and > (without =) false | |
| if (bOp == LeU || bOp == LeS || bOp == GeU || bOp == GeS) { | |
| return True; | |
| } | |
| if (bOp == LtU || bOp == LtS || bOp == GtU || bOp == GtS) { | |
| return False; | |
| } | |
| break; | |
| case LtS: | |
| // < proves <=, != true and ==, > false | |
| if (bOp == LeS || bOp == Ne) { | |
| return True; | |
| } | |
| if (bOp == Eq || bOp == GtS) { | |
| return False; | |
| } | |
| break; | |
| case GtS: | |
| // Ditto, with G instead of L. | |
| if (bOp == GeS || bOp == Ne) { | |
| return True; | |
| } | |
| if (bOp == Eq || bOp == LtS) { | |
| return False; | |
| } | |
| break; | |
| case LtU: | |
| // Ditto, with unsigned. | |
| if (bOp == LeU || bOp == Ne) { | |
| return True; | |
| } | |
| if (bOp == Eq || bOp == GtU) { | |
| return False; | |
| } | |
| break; | |
| case GtU: | |
| // Ditto, with G instead of L. | |
| if (bOp == GeU || bOp == Ne) { | |
| return True; | |
| } | |
| if (bOp == Eq || bOp == LtU) { | |
| return False; | |
| } | |
| break; | |
| default: { | |
| } | |
| } | |
| return Unknown; | |
| } | |
| // Core comparison of two constraints: whether a => b | |
| Result provesPair(const Constraint& a, const Constraint& b) { | |
| // A thing always implies itself. | |
| if (a == b) { | |
| return True; | |
| } | |
| // A thing always implies its negation is false. | |
| if (a == b.negate()) { | |
| return False; | |
| } | |
| // Comparisons of two constants. | |
| auto* aConstant = std::get_if<Literal>(&a.term); | |
| auto* bConstant = std::get_if<Literal>(&b.term); | |
| if (aConstant && bConstant) { | |
| auto result = provesConstantPair(a.op, *aConstant, b.op, *bConstant); | |
| if (result != Unknown) { | |
| return result; | |
| } | |
| } | |
| // If we can represent both as spans, we can calculate that way. At least one | |
| // must be a constant in this case, so that we know the type. | |
| if (aConstant || bConstant) { | |
| auto type = aConstant ? aConstant->type : bConstant->type; | |
| // Use proven spans for a, and exact for b. This allows us to do | |
| // a => proven spans for a => exact spans for b => b. | |
| if (auto aSpans = a.getProvenSpans(type)) { | |
| if (auto bSpans = b.getSpans(type)) { | |
| if (aSpans->empty()) { | |
| // An empty span implies a contradiction (e.g. x > MAX_INT), as it | |
| // means no possible number can apply. And contradictions prove | |
| // anything. | |
| return True; | |
| } | |
| if (bSpans->empty()) { | |
| // Anything that is not a contradiction can prove a contradiction. | |
| return False; | |
| } | |
| if (bSpans->contains(*aSpans)) { | |
| // b's values contains a's, e.g., b = { 0 < x < 10 } and | |
| // a = { 3 < x < 7 }, so a => b. | |
| return True; | |
| } | |
| if (!bSpans->hasOverlap(*aSpans)) { | |
| // There is no overlap at all, e.g., { 0 < x < 10 } vs { 20 < x < 30 | |
| // }, both cannot be true and each proves the other false. | |
| return False; | |
| } | |
| } | |
| } | |
| } | |
| if (a.term == b.term) { | |
| return provesTermEqualPair(a.op, b.op); | |
| } | |
| return Unknown; | |
| } | |
| } // anonymous namespace | |
| Result AndedConstraintSet::proves(const Constraint& condition) const { | |
| if (provesEverything()) { | |
| return True; | |
| } | |
| // Note we do not need to handle the provesNothing case in a special way: the | |
| // loop below finds nothing. | |
| // Sometimes a single constraint is enough to determine the condition. | |
| for (auto& c : *this) { | |
| auto result = provesPair(c, condition); | |
| if (result != Unknown) { | |
| return result; | |
| } | |
| } | |
| // TODO smarts for multiple constraints | |
| // Otherwise, who knows. | |
| return Unknown; | |
| } | |
| Result AndedConstraintSet::proves(const AndedConstraintSet& other) const { | |
| if (provesEverything()) { | |
| return True; | |
| } | |
| if (other.provesEverything()) { | |
| // We are not a contradiction, but other is, so we prove it false. | |
| return False; | |
| } | |
| bool hasUnknown = false; | |
| for (auto& c : other) { | |
| auto result = proves(c); | |
| if (result == False) { | |
| // The entire conjunction is proven false. | |
| return False; | |
| } | |
| if (result == Unknown) { | |
| hasUnknown = true; | |
| } | |
| } | |
| return hasUnknown ? Unknown : True; | |
| } | |
| namespace { | |
| // Do an AND on a pair of constraints, looking for a way to fuse them together | |
| // into a single constraint that represents them both, while assuming the | |
| // constraints have an equal term. If we fail, return nullopt. | |
| std::optional<Constraint> fusedApproximateAndTermEqualPair( | |
| const Abstract::Op aOp, const Abstract::Op bOp, const Term& term) { | |
| using namespace Abstract; | |
| // x < C && x <= C === x < C | |
| if (aOp == LtS && bOp == LeS) { | |
| return Constraint{LtS, term}; | |
| } | |
| if (aOp == LtU && bOp == LeU) { | |
| return Constraint{LtU, term}; | |
| } | |
| // TODO: all the rest | |
| return {}; | |
| } | |
| // Do an AND on a pair of constraints, looking for a way to fuse them together | |
| // into a single constraint that represents them both. If we fail, return | |
| // nullopt. | |
| std::optional<Constraint> fusedApproximateAndPair(const Constraint& a, | |
| const Constraint& b, | |
| bool recursing = false) { | |
| // If a proves b is true, all we need is a (e.g. { x == 5 && x > 0 } => x == 5 | |
| if (provesPair(a, b) == True) { | |
| return a; | |
| } | |
| if (a.term == b.term) { | |
| if (auto result = fusedApproximateAndTermEqualPair(a.op, b.op, a.term)) { | |
| return result; | |
| } | |
| } | |
| if (!recursing) { | |
| // The flipped form may be recognized. | |
| return fusedApproximateAndPair(b, a, true); | |
| } | |
| return {}; | |
| } | |
| bool isImmediateContradiction(const Constraint& c) { | |
| using namespace Abstract; | |
| auto* cc = std::get_if<Literal>(&c.term); | |
| if (!cc) { | |
| // Only operations on constants can be immediate contradictions. | |
| return false; | |
| } | |
| if (!cc->type.isInteger()) { | |
| return false; | |
| } | |
| auto minSigned = cc->type == Type::i32 ? std::numeric_limits<int32_t>::min() | |
| : std::numeric_limits<int64_t>::min(); | |
| auto maxSigned = cc->type == Type::i32 ? std::numeric_limits<int32_t>::max() | |
| : std::numeric_limits<int64_t>::max(); | |
| auto maxUnsigned = cc->type == Type::i32 | |
| ? std::numeric_limits<uint32_t>::max() | |
| : std::numeric_limits<uint64_t>::max(); | |
| switch (c.op) { | |
| case LtS: | |
| if (cc->getInteger() == minSigned) { | |
| // Less than the lowest possible number. | |
| return true; | |
| } | |
| break; | |
| case LtU: | |
| if (cc->getInteger() == 0) { | |
| // Less than the lowest possible number. | |
| return true; | |
| } | |
| break; | |
| case GtS: | |
| if (cc->getInteger() == maxSigned) { | |
| // Greater than the highest possible number. | |
| return true; | |
| } | |
| break; | |
| case GtU: | |
| if (cc->getUnsigned() == maxUnsigned) { | |
| // Greater than the highest possible number. | |
| return true; | |
| } | |
| break; | |
| default: { | |
| } | |
| } | |
| return false; | |
| } | |
| } // anonymous namespace | |
| void AndedConstraintSet::approximateAnd(const Constraint& c) { | |
| if (provesEverything()) { | |
| // Nothing to add. | |
| return; | |
| } | |
| // We don't store contradictions: identify them and mark us as such. | |
| if (isImmediateContradiction(c)) { | |
| setProvesEverything(); | |
| return; | |
| } | |
| auto result = proves(c); | |
| if (result == True) { | |
| // We already prove c to be true, so it adds nothing. | |
| return; | |
| } else if (result == False) { | |
| // We are now a contradiction. | |
| setProvesEverything(); | |
| return; | |
| } | |
| for (auto& existing : *this) { | |
| // Some ANDed constraints fuse together into a new constraint. | |
| if (auto fused = fusedApproximateAndPair(existing, c)) { | |
| existing = *fused; | |
| // Sort to ensure we are in the right place. | |
| std::sort(begin(), end()); | |
| return; | |
| } | |
| } | |
| // TODO: use Spans here when possible | |
| if (size() < MaxConstraints) { | |
| // Insert into the right place, keeping us sorted. | |
| insert(std::upper_bound(begin(), end(), c), c); | |
| return; | |
| } | |
| // Otherwise, just do not add this one. | |
| // TODO: We could try to be clever and see if one of the existing ones makes | |
| // more sense to drop. In particular, we should prefer "better" ones | |
| // like > over >= and so forth (sorting more precise ones earlier may be | |
| // useful to implement that). | |
| } | |
| namespace { | |
| // Do an OR of a pair of constraints where the terms are known to be equal. If | |
| // we can't find a good way to express their ORing, return nullopt. | |
| std::optional<Constraint> approximateOrTermEqualPair(const Abstract::Op aOp, | |
| const Abstract::Op bOp, | |
| const Term& term) { | |
| using namespace Abstract; | |
| // x == C || x > C === x >= C | |
| if (aOp == Eq && bOp == GtS) { | |
| return Constraint{GeS, term}; | |
| } | |
| if (aOp == Eq && bOp == GtU) { | |
| return Constraint{GeU, term}; | |
| } | |
| // x > C || x >= C === x >= C | |
| if (aOp == GtS && bOp == GeS) { | |
| return Constraint{GeS, term}; | |
| } | |
| if (aOp == GtU && bOp == GeU) { | |
| return Constraint{GeU, term}; | |
| } | |
| // TODO: all the rest | |
| return {}; | |
| } | |
| // Do an OR of a pair of constraints where the terms are adjacent constants: a | |
| // operates on C, and b on C+1. | |
| std::optional<Constraint> approximateOrAdjacentConstantPair( | |
| const Abstract::Op aOp, const Literal& aConstant, const Abstract::Op bOp) { | |
| using namespace Abstract; | |
| // x == C || x >= C+1 === x >= C, if C+1 does not overflow. | |
| if (aOp == Eq && bOp == GeS && !aConstant.isSignedMax()) { | |
| return Constraint{GeS, {aConstant}}; | |
| } | |
| if (aOp == Eq && bOp == GeU && !aConstant.isUnsignedMax()) { | |
| return Constraint{GeU, {aConstant}}; | |
| } | |
| // x > C || x >= C+1 === x > C, if C+1 does not overflow. | |
| if (aOp == GtS && bOp == GeS && !aConstant.isSignedMax()) { | |
| return Constraint{GtS, {aConstant}}; | |
| } | |
| if (aOp == GtU && bOp == GeU && !aConstant.isUnsignedMax()) { | |
| return Constraint{GtU, {aConstant}}; | |
| } | |
| // TODO: all the rest | |
| return {}; | |
| } | |
| // Do an OR of a pair of constraints. If we can't find a good way to express | |
| // their ORing, return nullopt. | |
| std::optional<Constraint> approximateOrPair(const Constraint& a, | |
| const Constraint& b, | |
| bool recursing = false) { | |
| if (a.term == b.term) { | |
| if (auto result = approximateOrTermEqualPair(a.op, b.op, a.term)) { | |
| return result; | |
| } | |
| } | |
| // See if we operate on constants N, N+1. | |
| if (auto* ac = std::get_if<Literal>(&a.term)) { | |
| if (auto* bc = std::get_if<Literal>(&b.term)) { | |
| if (ac->type == bc->type && ac->type.isInteger() && | |
| ac->add(Literal::makeFromInt32(1, ac->type)) == *bc) { | |
| if (auto result = approximateOrAdjacentConstantPair(a.op, *ac, b.op)) { | |
| return result; | |
| } | |
| } | |
| } | |
| } | |
| // If a proves b, e.g. x = 5 proves x >= 0 is true, then the OR is b. | |
| if (provesPair(a, b) == True) { | |
| return b; | |
| } | |
| // TODO: more smarts | |
| if (!recursing) { | |
| // The flipped form may be recognized. | |
| return approximateOrPair(b, a, true); | |
| } | |
| return {}; | |
| } | |
| // Do an OR in full detail, looking at every constraint in each of the given | |
| // sets. | |
| AndedConstraintSet detailedApproximateOr(const AndedConstraintSet& a, | |
| const AndedConstraintSet& b) { | |
| // We can process this in full detail by looking at all the combinations of | |
| // individual constraints, because of the distributive property: | |
| // | |
| // (A & B) | (C & D) == ((A & B) | C) & ((A & B) | D) | |
| // == (A | C) & (B | C) & (A | D) & (B | D) | |
| // | |
| // This is quadratic, but constraint sets are limited to a very small size, | |
| // making this reasonable. | |
| // | |
| // Also, note that we don't need to worry about new contradictions here: ORing | |
| // things never leads to a contradiction, and we can assume the inputs are | |
| // not contradictions. | |
| assert(!a.provesEverything() && !b.provesEverything()); | |
| auto result = AndedConstraintSet::makeProvesNothing(); | |
| for (auto& ac : a) { | |
| for (auto& bc : b) { | |
| if (auto combined = approximateOrPair(ac, bc)) { | |
| // We found something useful by ORing them, keep it. | |
| result.approximateAnd(*combined); | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| } // anonymous namespace | |
| bool AndedConstraintSet::approximateOr(const AndedConstraintSet& other) { | |
| // If one proves everything, the only thing that matters is the other. | |
| if (other.provesEverything()) { | |
| return false; | |
| } | |
| if (provesEverything()) { | |
| *this = other; | |
| return true; | |
| } | |
| // If this is already implied by current constraints, then it is redundant. | |
| // E.g. if we are { x = 10 } and other is { x >= 0 } then all we need is | |
| // { x >= 0 } as the result of the OR. | |
| if (other.proves(*this) == True) { | |
| return false; | |
| } | |
| if (proves(other) == True) { | |
| *this = other; | |
| return true; | |
| } | |
| // TODO: use Spans here when possible | |
| // For more complex cases, do a detailed analysis. | |
| auto result = detailedApproximateOr(*this, other); | |
| auto changed = (result != *this); | |
| *this = result; | |
| return changed; | |
| } | |
| namespace { | |
| // Internal parsing, utilizing a map of tees. We can handle tees in an | |
| // expression, so long as they do not interfere with each other: | |
| // | |
| // (i32.eq | |
| // (local.tee $x ..) | |
| // (i32.const 10) | |
| // ) | |
| // | |
| // We can parse this into $x == 10, just as if we saw a local.get $x there. But | |
| // we cannot handle this: | |
| // | |
| // (i32.eq | |
| // (local.get $x) | |
| // (local.tee $x ..) | |
| // ) | |
| // | |
| // Parsing this into $x == $x would be wrong: the first $x is the old value. | |
| // That is, fully handling tees requires a more SSA-like IR. To handle the | |
| // common cases we want to, we parse the code in the natural order of execution, | |
| // and maintain a list of local operations. A get before a tee indicates | |
| // possible interference. | |
| struct LocalOperations { | |
| SmallVector<Expression*, 10> vec; | |
| // Check if an Expression returns a local's value: it is either a get or a | |
| // tee. Returns the index and type if so, and notes it in our vector. | |
| struct LocalOperation { | |
| Index index; | |
| }; | |
| std::optional<LocalOperation> parse(Expression* curr) { | |
| auto handleNestedSets = [&](Expression* from) { | |
| for (auto* nested : FindAll<LocalSet>(from).list) { | |
| vec.push_back(nested); | |
| } | |
| }; | |
| if (auto* get = curr->dynCast<LocalGet>()) { | |
| vec.push_back(get); | |
| return LocalOperation{get->index}; | |
| } | |
| if (auto* set = curr->dynCast<LocalSet>()) { | |
| // We are parsing expressions in a tree, not none-typed items in a block. | |
| assert(set->isTee()); | |
| // We know the value of this expression - the local the tee writes to - | |
| // but further sets may be nested in the value, affecting other locals. | |
| handleNestedSets(set->value); | |
| // Ignore unreachable code, so the callers don't need to handle it. | |
| if (set->type == Type::unreachable) { | |
| return {}; | |
| } | |
| vec.push_back(set); | |
| return LocalOperation{set->index}; | |
| } | |
| // Unrecognized. As above, we must scan for nested sets. | |
| handleNestedSets(curr); | |
| return {}; | |
| } | |
| // Check for any possible interference between locals, which would tell the | |
| // caller that whatever was parsed is not valid. | |
| bool hasLocalInterference() const { | |
| if (vec.size() <= 1) { | |
| return false; | |
| } | |
| // Process the list in detail, as interference - a get before a set of the | |
| // same local - is possible. We track the read locals, and if we see a | |
| // later write, that shows a problem. | |
| std::unordered_set<Index> read; | |
| for (auto* curr : vec) { | |
| if (auto* get = curr->dynCast<LocalGet>()) { | |
| read.insert(get->index); | |
| } else if (auto* set = curr->dynCast<LocalSet>()) { | |
| if (read.contains(set->index)) { | |
| return true; | |
| } | |
| // Insert a read, because the tee does both a write and a read. | |
| if (set->isTee()) { | |
| read.insert(set->index); | |
| } | |
| } else { | |
| WASM_UNREACHABLE("invalid local op"); | |
| } | |
| } | |
| return false; | |
| } | |
| }; | |
| std::optional<LocalConstraint> | |
| localConstraintParseInternal(Expression* curr, | |
| LocalOperations& localOperations) { | |
| auto parseEqZArgument = | |
| [&](Expression* value) -> std::optional<LocalConstraint> { | |
| if (auto parsed = localConstraintParseInternal(value, localOperations)) { | |
| return parsed->negate(); | |
| } | |
| if (auto localOp = localOperations.parse(value)) { | |
| // Canonicalize EqZ to Eq of 0. | |
| auto zero = Literal::makeZero(value->type); | |
| return LocalConstraint{localOp->index, Constraint{Abstract::Eq, {zero}}}; | |
| } | |
| return {}; | |
| }; | |
| if (auto* u = curr->dynCast<Unary>()) { | |
| if (Abstract::getUnary(u->value->type, Abstract::EqZ) == u->op) { | |
| return parseEqZArgument(u->value); | |
| } | |
| return {}; | |
| } | |
| if (auto* refIsNull = curr->dynCast<RefIsNull>()) { | |
| return parseEqZArgument(refIsNull->value); | |
| } | |
| // Parse a get, tee, or a constant. | |
| auto parseTerm = [&](Expression* expr) -> std::optional<Term> { | |
| if (auto localOp = localOperations.parse(expr)) { | |
| return Term{localOp->index}; | |
| } | |
| if (Properties::isSingleConstantExpression(expr)) { | |
| return Term{Properties::getLiteral(expr)}; | |
| } | |
| return {}; | |
| }; | |
| auto parseBinaryArguments = | |
| [&](Abstract::Op op, | |
| Expression* left, | |
| Expression* right) -> std::optional<LocalConstraint> { | |
| // The left must be a get or a tee. | |
| if (auto localOp = localOperations.parse(left)) { | |
| // The right can be any term. | |
| if (auto value = parseTerm(right)) { | |
| return LocalConstraint{localOp->index, Constraint{op, *value}}; | |
| } | |
| } | |
| return {}; | |
| }; | |
| if (auto* binary = curr->dynCast<Binary>()) { | |
| // The operation must be one we recognize. | |
| for (auto op : {Abstract::Eq, | |
| Abstract::Ne, | |
| Abstract::LtS, | |
| Abstract::LtU, | |
| Abstract::LeS, | |
| Abstract::LeU, | |
| Abstract::GtS, | |
| Abstract::GtU, | |
| Abstract::GeS, | |
| Abstract::GeU}) { | |
| if (Abstract::getBinary(binary->left->type, op) == binary->op) { | |
| return parseBinaryArguments(op, binary->left, binary->right); | |
| } | |
| } | |
| return {}; | |
| } | |
| if (auto* refEq = curr->dynCast<RefEq>()) { | |
| return parseBinaryArguments(Abstract::Eq, refEq->left, refEq->right); | |
| } | |
| return {}; | |
| } | |
| } // anonymous namespace | |
| std::optional<LocalConstraint> LocalConstraint::parse(Expression* curr) { | |
| LocalOperations localOperations; | |
| auto ret = localConstraintParseInternal(curr, localOperations); | |
| if (localOperations.hasLocalInterference()) { | |
| return {}; | |
| } | |
| return ret; | |
| } | |
| ParsedAndedConstraints ParsedAndedConstraints::parse(Expression* curr) { | |
| using namespace Match; | |
| // The final return value. | |
| ParsedAndedConstraints ret; | |
| // We will track local operations over the entire tree we parse. | |
| LocalOperations localOperations; | |
| // Starting from |curr|, parse and recurse into sub-trees: when we see an AND, | |
| // we push both children as further work. | |
| SmallVector<Expression*, 4> work; | |
| work.push_back(curr); | |
| while (!work.empty()) { | |
| auto* curr = work.back(); | |
| work.pop_back(); | |
| auto parsed = localConstraintParseInternal(curr, localOperations); | |
| if (parsed) { | |
| ret.push_back(*parsed); | |
| continue; | |
| } | |
| Binary* b; | |
| if (matches(curr, binary(&b, Abstract::And, any(), any()))) { | |
| // An AND can be recursively processed: both sides must be true. Push them | |
| // in reverse order, so we process them in the natural order of execution. | |
| work.push_back(b->right); | |
| work.push_back(b->left); | |
| continue; | |
| } | |
| // TODO: support OR | |
| // We failed to parse this as constraints. We do still need to check for | |
| // local operations that might interfere with the things we did parse, | |
| // otherwise. | |
| localOperations.parse(curr); | |
| ret.hasUnknown = true; | |
| } | |
| if (localOperations.hasLocalInterference()) { | |
| return {}; | |
| } | |
| return ret; | |
| } | |
| ParsedAndedConstraints | |
| ParsedAndedConstraints::parseCondition(Expression* curr) { | |
| // A get or tee by itself is a check for not being null. | |
| LocalOperations localOperations; | |
| if (auto localOp = localOperations.parse(curr)) { | |
| auto value = Literal::makeZero(curr->type); | |
| return {LocalConstraint{localOp->index, Constraint{Abstract::Ne, {value}}}}; | |
| } | |
| // Otherwise, parse normally. | |
| return parse(curr); | |
| } | |
| void ParsedAndedConstraints::negate() { | |
| if (empty()) { | |
| return; | |
| } | |
| if (hasUnknown) { | |
| // This includes things we don't know about, and don't know how to negate. | |
| clear(); | |
| return; | |
| } | |
| // The input is a list of constraints all applying at once, A & B & C. The | |
| // negation is !A | !B | !C, but we cannot express a general OR like that, | |
| // except in the simple case where they all talk about the same local: then | |
| // we can at least approximateOr them all into one constraint. | |
| auto& self = *this; | |
| for (Index i = 1; i < size(); i++) { | |
| if (self[i].local != self[0].local) { | |
| // They refer to different locals. Give up. | |
| clear(); | |
| return; | |
| } | |
| } | |
| // Negate them before the OR. | |
| for (auto& pair : self) { | |
| pair.constraint = pair.constraint.negate(); | |
| } | |
| if (size() == 1) { | |
| // The simple case of 1 doesn't need any more work. | |
| return; | |
| } | |
| // Do the OR. | |
| AndedConstraintSet anded; | |
| anded.set(self[0].constraint); | |
| for (Index i = 1; i < size(); i++) { | |
| anded.approximateOr({self[i].constraint}); | |
| if (anded.provesNothing()) { | |
| // We have nothing useful here. | |
| clear(); | |
| return; | |
| } | |
| } | |
| // Return only the OR'ed result. | |
| auto local = self[0].local; | |
| clear(); | |
| for (auto& c : anded) { | |
| emplace_back(local, c); | |
| } | |
| } | |
| void LocalConstraint::flip() { | |
| auto other = std::get<Index>(constraint.term); | |
| constraint.term = Term{local}; | |
| local = other; | |
| if (Abstract::isRelationalAntisymmetric(constraint.op)) { | |
| constraint.op = Abstract::flipRelational(constraint.op); | |
| } else { | |
| // All we support for now are symmetric and antisymmetric operations. | |
| assert(Abstract::isRelationalSymmetric(constraint.op)); | |
| } | |
| } | |
| void BasicBlockConstraintMap::set(Index index, const Constraint& c) { | |
| set(index, AndedConstraintSet{c}); | |
| } | |
| void BasicBlockConstraintMap::set(Index index, | |
| const AndedConstraintSet& constraints) { | |
| // We should not set values in unreachable code. | |
| assert(!unreachable); | |
| // Clear the old state. | |
| eraseStaleRefs(index); | |
| map.erase(index); | |
| // Apply the constraints, if there are any. | |
| if (constraints.provesNothing()) { | |
| setProvesNothing(index); | |
| } else { | |
| for (auto& c : constraints) { | |
| approximateAnd(index, c); | |
| } | |
| } | |
| } | |
| void BasicBlockConstraintMap::set(Index index, Expression* value) { | |
| using namespace Match; | |
| using namespace Abstract; | |
| // Apply a constraint to a value, x = C. | |
| if (Properties::isSingleConstantExpression(value)) { | |
| auto c = Properties::getLiteral(value); | |
| set(index, Constraint{Eq, {c}}); | |
| return; | |
| } | |
| // Apply a constraint to a local, x = y. | |
| if (auto* get = value->dynCast<LocalGet>()) { | |
| set(index, Constraint{Eq, {get->index}}); | |
| return; | |
| } | |
| if (auto* tee = value->dynCast<LocalSet>()) { | |
| set(index, Constraint{Eq, {tee->index}}); | |
| return; | |
| } | |
| // Apply an increment of a local, x = y + 1. | |
| Index y; | |
| if (matches(value, binary(Add, local(&y), ival(1)))) { | |
| // The local y must have old constraints that we know how to increment and | |
| // transform into new ones. | |
| const auto old = get(y); | |
| auto new_ = old; | |
| // If we see an unsigned upper bound but not a lower one, we can add a | |
| // lower one (if we do not overflow). That is, if we see x < 100, x++, then | |
| // we can not only update x < 100 to x <= 100, but also add x > 0 (since 0 | |
| // is impossible after the ++). This is not possible for signed operations, | |
| // since x++ does not prove x > 0 there (0 is not the only value that is | |
| // <= 0). | |
| bool hasUnsignedUpperBound = false; | |
| Type type; | |
| // Iterate over the old constraints and increment each one. | |
| for (auto iter = new_.begin(); iter != new_.end();) { | |
| auto& c = *iter; | |
| auto* N = std::get_if<Literal>(&c.term); | |
| if (!N) { | |
| // A non-constant term, which we don't know how to increment. Simply | |
| // remove it: we are losing proving power here, but doing so is never | |
| // invalid. | |
| iter = new_.erase(iter); | |
| continue; | |
| } | |
| type = N->type; | |
| switch (c.op) { | |
| // x == N, x++ => x == N+1. | |
| case Eq: | |
| *N = N->add(Literal::makeFromInt32(1, N->type)); | |
| break; | |
| // x >= N, x++ => x > N if no overflow | |
| case GeS: | |
| if (old.proves({LtS, {Literal::makeSignedMax(N->type)}}) != True) { | |
| iter = new_.erase(iter); | |
| continue; | |
| } | |
| c.op = GtS; | |
| break; | |
| case GeU: | |
| if (old.proves({LtU, {Literal::makeUnsignedMax(N->type)}}) != True) { | |
| iter = new_.erase(iter); | |
| continue; | |
| } | |
| c.op = GtU; | |
| break; | |
| // x < N, x++ => x <= N | |
| case LtS: | |
| c.op = LeS; | |
| break; | |
| case LtU: | |
| c.op = LeU; | |
| hasUnsignedUpperBound = true; | |
| break; | |
| // x <= N, x++ => x <= N+1 if no overflow | |
| case LeS: | |
| if (N->isSignedMax()) { | |
| iter = new_.erase(iter); | |
| continue; | |
| } | |
| *N = N->add(Literal::makeFromInt32(1, N->type)); | |
| break; | |
| case LeU: | |
| if (N->isUnsignedMax()) { | |
| iter = new_.erase(iter); | |
| continue; | |
| } | |
| *N = N->add(Literal::makeFromInt32(1, N->type)); | |
| hasUnsignedUpperBound = true; | |
| break; | |
| default: | |
| // Something we don't recognize. | |
| iter = new_.erase(iter); | |
| continue; | |
| } | |
| ++iter; | |
| } | |
| if (hasUnsignedUpperBound) { | |
| // We know we did not overflow (we are bounded from above), so add x > 0. | |
| new_.approximateAnd({GtU, {Literal::makeFromInt32(0, type)}}); | |
| } | |
| set(index, new_); | |
| return; | |
| } | |
| // We know and can prove nothing. | |
| setProvesNothing(index); | |
| } | |
| void BasicBlockConstraintMap::setProvesNothing(Index index) { | |
| assert(!unreachable); | |
| eraseStaleRefs(index); | |
| map.erase(index); | |
| } | |
| bool BasicBlockConstraintMap::approximateOr( | |
| const BasicBlockConstraintMap& other) { | |
| // If one is unreachable, it adds nothing to the other. | |
| if (other.unreachable) { | |
| return false; | |
| } | |
| if (unreachable) { | |
| *this = other; | |
| return true; | |
| } | |
| // We only need to loop on our locals, as any local that is missing in us is | |
| // one that would end up proving nothing (and get removed). | |
| bool changed = false; | |
| for (auto& [local, constraints] : map) { | |
| changed |= constraints.approximateOr(other.get(local)); | |
| } | |
| // Anything that became trivial after the OR must be removed. | |
| std::erase_if(map, [&](const auto& item) { | |
| const auto& [local, constraints] = item; | |
| // We do not store contradictions. | |
| assert(!constraints.provesEverything()); | |
| if (constraints.provesNothing()) { | |
| changed = true; | |
| return true; | |
| } | |
| return false; | |
| }); | |
| return changed; | |
| } | |
| void BasicBlockConstraintMap::approximateAndInternal(Index index, | |
| const Constraint& c, | |
| bool flip, | |
| bool isCopy) { | |
| // We should not be applying constraints when already unreachable. | |
| assert(!unreachable); | |
| Constraint actual = c; | |
| if (flip) { | |
| LocalConstraint flipped{index, c}; | |
| flipped.flip(); | |
| index = flipped.local; | |
| actual = flipped.constraint; | |
| } | |
| if (auto* other = std::get_if<Index>(&actual.term)) { | |
| // Never add constraints to ourselves (x == x, etc., which can happen due to | |
| // copying/flipping). | |
| if (*other == index) { | |
| return; | |
| } | |
| // If we are applying a constraint to another local, and we know that | |
| // local's value, propagate it. That is, if x == 42, then if we try to apply | |
| // y < x we instead apply y < 42, which is better. | |
| auto otherConstraints = get(*other); | |
| if (auto lit = otherConstraints.getLiteral()) { | |
| actual.term = Term{*lit}; | |
| } | |
| } | |
| // Refer to the constraints for this index. If this is the first access of | |
| // the local, then we insert a new item into the map, which has a default of | |
| // proxesEverything, which we need to flip (provesEverything cannot otherwise | |
| // be found in the map, as we never store it). | |
| auto [iter, _] = map.insert({index, AndedConstraintSet::makeProvesNothing()}); | |
| auto& indexConstraints = iter->second; | |
| // As in ::set(), this makes the map temporarily invalid until the | |
| // approximateAnd, as we don't store proves-nothing in the map, normally. | |
| indexConstraints.approximateAnd(actual); | |
| if (indexConstraints.provesEverything()) { | |
| // We just proved we are in unreachable code. | |
| unreachable = true; | |
| map.clear(); | |
| return; | |
| } | |
| // We just added a constraint, so we can prove something (we may lose some | |
| // information as this is an approximate AND, but we cannot lose it all). | |
| assert(!indexConstraints.provesNothing()); | |
| // Add a ref of what we are adding. Note that the approximation above may end | |
| // up not actually adding this, or adding only part of this, but it is safe to | |
| // always add a ref (at the cost of minor wasted work). | |
| noteRefs(index, actual); | |
| // If this is not the flipped version, and it refers to a local, add the | |
| // flipped one too. | |
| if (!flip && std::holds_alternative<Index>(actual.term)) { | |
| approximateAndInternal(index, actual, true, isCopy); | |
| if (unreachable) { | |
| // We just found a contradiction. | |
| return; | |
| } | |
| } | |
| // If this constraint is simply "== x", then we are equal to that other local | |
| // x, and can copy its constraints (if we are not already such a copy). | |
| if (!isCopy) { | |
| if (auto* other = std::get_if<Index>(&actual.term)) { | |
| if (actual.op == Abstract::Eq) { | |
| for (auto& otherC : get(*other)) { | |
| approximateAndInternal(index, otherC, false, true); | |
| if (unreachable) { | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Result BasicBlockConstraintMap::proves(LocalConstraint condition) const { | |
| // As in approximateAnd, above, if the term is a local that we know something | |
| // about, propagate it. TODO: even without equality, we can add more | |
| // constraints here (e.g. x < y and y < 10 can lead to proving x < 10) | |
| if (auto* other = std::get_if<Index>(&condition.constraint.term)) { | |
| auto otherConstraints = get(*other); | |
| if (auto lit = otherConstraints.getLiteral()) { | |
| condition.constraint.term = Term{*lit}; | |
| } | |
| } | |
| return get(condition.local).proves(condition.constraint); | |
| } | |
| void BasicBlockConstraintMap::noteRefs(Index index, const Constraint& c) { | |
| if (auto* i = std::get_if<Index>(&c.term)) { | |
| refs[*i].insert(index); | |
| } | |
| } | |
| void BasicBlockConstraintMap::eraseStaleRefs(Index index) { | |
| auto iter = refs.find(index); | |
| if (iter == refs.end()) { | |
| return; | |
| } | |
| auto& refIndexes = iter->second; | |
| for (auto refIndex : refIndexes) { | |
| if (auto iter = map.find(refIndex); iter != map.end()) { | |
| auto& refConstraints = iter->second; | |
| std::erase_if(refConstraints, [&](const auto& c) { | |
| if (auto* i = std::get_if<Index>(&c.term)) { | |
| if (*i == index) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| }); | |
| if (refConstraints.empty()) { | |
| // This became trivial. | |
| map.erase(iter); | |
| } | |
| } | |
| } | |
| } | |
| std::ostream& operator<<(std::ostream& o, const Constraint& c) { | |
| o << "Constraint{" << c.op << ", "; | |
| if (auto* cc = std::get_if<Literal>(&c.term)) { | |
| o << *cc; | |
| } else if (auto* i = std::get_if<Index>(&c.term)) { | |
| o << "$" << *i; | |
| } | |
| o << '}'; | |
| return o; | |
| } | |
| std::ostream& operator<<(std::ostream& o, const LocalConstraint& c) { | |
| o << "LocalConstraint{$" << c.local << ", " << c.constraint << '}'; | |
| return o; | |
| } | |
| std::ostream& operator<<(std::ostream& o, const AndedConstraintSet& set) { | |
| if (set.provesEverything()) { | |
| o << "AndedConstraintSet(contradiction)"; | |
| return o; | |
| } | |
| o << "AndedConstraintSet{"; | |
| bool first = true; | |
| for (auto& constraint : set) { | |
| if (first) { | |
| first = false; | |
| } else { | |
| o << ", "; | |
| } | |
| o << constraint; | |
| } | |
| o << '}'; | |
| return o; | |
| } | |
| std::ostream& operator<<(std::ostream& o, const BasicBlockConstraintMap& map) { | |
| if (map.unreachable) { | |
| o << "BasicBlockConstraintMap(unreachable)"; | |
| return o; | |
| } | |
| o << "BasicBlockConstraintMap{"; | |
| bool first = true; | |
| for (auto& [local, constraints] : map.map) { | |
| if (first) { | |
| first = false; | |
| } else { | |
| o << ", "; | |
| } | |
| o << local << ": " << constraints; | |
| } | |
| o << '}'; | |
| return o; | |
| } | |
| } // namespace wasm::constraint |