| #ifndef wasm_analysis_stack_lattice_h |
| #define wasm_analysis_stack_lattice_h |
| |
| #include <deque> |
| #include <optional> |
| |
| #include "lattice.h" |
| |
| namespace wasm::analysis { |
| |
| // Note that in comments, bottom is left and top is right. |
| |
| // This lattice models the behavior of a stack of values. The lattice |
| // is templated on StackElementLattice, which is a lattice which can |
| // model some abstract property of a value on the stack. The StackLattice |
| // itself can push or pop abstract values and access the top of stack. |
| // |
| // The goal here is not to operate directly on the stacks. Rather, the |
| // StackLattice organizes the StackElementLattice elements in an efficient |
| // and natural way which reflects the behavior of the wasm value stack. |
| // Transfer functions will operate on stack elements individually. The |
| // stack itself is an intermediate structure. |
| // |
| // Comparisons are done elementwise, starting from the top of the stack. |
| // For instance, to compare the stacks [c,b,a], [b',a'], we first compare |
| // a with a', then b with b'. Then we make note of the fact that the first |
| // stack is higher, with an extra c element at the bottom. |
| // |
| // Similarly, Least Upper Bounds are done elementwise starting from the top. |
| // For instance LUB([b, a], [b', a']) = [LUB(b, b'), LUB(a, a')], while |
| // LUB([c, b, a], [b', a']) = [c, LUB(b, b'), LUB(a, a')]. |
| // |
| // These are done from the top of the stack because this addresses the problem |
| // of scopes. For instance, if we have the following program |
| // |
| // i32.const 0 |
| // i32.const 0 |
| // if (result i32) |
| // i32.const 1 |
| // else |
| // i32.const 2 |
| // end |
| // i32.add |
| // |
| // Before the if-else control flow, we have [] -> [i32], and after the if-else |
| // control flow we have [i32, i32] -> [i32]. However, inside each of the if |
| // and else conditions, we have [] -> [i32], because they cannot see the |
| // stack elements pushed by the enclosing scope. In effect in the if and else, |
| // we have a stack [i32 | i32], where we can't "see" left of the |. |
| // |
| // Conceptually, we can also imagine each stack [b, a] as being implicitly an |
| // infinite stack of the form (bottom) [... BOTTOM, BOTTOM, b, a] (top). This |
| // makes stacks in different scopes comparable, with only their contents |
| // different. Stacks in more "inner" scopes simply have more bottom elements in |
| // the bottom portion. |
| // |
| // A common application for this lattice is modeling the Wasm value stack. |
| // For instance, one could use this to analyze the maximum bit size of |
| // values on the Wasm value stack. When new actual values are pushed |
| // or popped off the Wasm value stack by instructions, the same is done |
| // to abstract lattice elements in the StackLattice. |
| // |
| // When two control flows are joined together, one with stack [b, a] and |
| // another with stack [b, a'], we can take the least upper bound to |
| // produce a stack [b, LUB(a, a')], where LUB(a, a') takes the maximum |
| // of the two maximum bit values. |
| |
| template<typename StackElementLattice> class StackLattice { |
| static_assert(is_lattice<StackElementLattice>); |
| StackElementLattice& stackElementLattice; |
| |
| public: |
| StackLattice(StackElementLattice& stackElementLattice) |
| : stackElementLattice(stackElementLattice) {} |
| |
| class Element { |
| // The top lattice can be imagined as an infinitely high stack of top |
| // elements, which is unreachable in most cases. In practice, we make the |
| // stack an optional, and we represent top with the absence of a stack. |
| std::optional<std::deque<typename StackElementLattice::Element>> |
| stackValue = std::deque<typename StackElementLattice::Element>(); |
| |
| public: |
| bool isTop() const { return !stackValue.has_value(); } |
| bool isBottom() const { |
| return stackValue.has_value() && stackValue->empty(); |
| } |
| void setToTop() { stackValue.reset(); } |
| |
| typename StackElementLattice::Element& stackTop() { |
| return stackValue->back(); |
| } |
| |
| void push(typename StackElementLattice::Element&& element) { |
| // We can imagine each stack [b, a] as being implicitly an infinite stack |
| // of the form (bottom) [... BOTTOM, BOTTOM, b, a] (top). In that case, |
| // adding a bottom element to an empty stack changes nothing, so we don't |
| // actually add a bottom. |
| if (stackValue.has_value() && |
| (!stackValue->empty() || !element.isBottom())) { |
| stackValue->push_back(std::move(element)); |
| } |
| } |
| |
| void push(const typename StackElementLattice::Element& element) { |
| if (stackValue.has_value() && |
| (!stackValue->empty() || !element.isBottom())) { |
| stackValue->push_back(std::move(element)); |
| } |
| } |
| |
| typename StackElementLattice::Element pop() { |
| typename StackElementLattice::Element value = stackValue->back(); |
| stackValue->pop_back(); |
| return value; |
| } |
| |
| // When taking the LUB, we take the LUBs of the elements of each stack |
| // starting from the top of the stack. So, LUB([b, a], [b', a']) is |
| // [LUB(b, b'), LUB(a, a')]. If one stack is higher than the other, |
| // the bottom of the higher stack will be kept, while the LUB of the |
| // corresponding tops of each stack will be taken. For instance, |
| // LUB([d, c, b, a], [b', a']) is [d, c, LUB(b, b'), LUB(a, a')]. |
| // |
| // We start at the top because it makes taking the LUB of stacks with |
| // different scope easier, as mentioned at the top of the file. It also |
| // fits with the conception of the stack starting at the top and having |
| // an infinite bottom, which allows stacks of different height and scope |
| // to be easily joined. |
| bool makeLeastUpperBound(const Element& other) { |
| // Top element cases, since top elements don't actually have the stack |
| // value. |
| if (isTop()) { |
| return false; |
| } else if (other.isTop()) { |
| stackValue.reset(); |
| return true; |
| } |
| |
| bool modified = false; |
| |
| // Merge the shorter height stack with the top of the longer height |
| // stack. We do this by taking the LUB of each pair of matching elements |
| // from both stacks. |
| auto otherIterator = other.stackValue->crbegin(); |
| auto thisIterator = stackValue->rbegin(); |
| for (; thisIterator != stackValue->rend() && |
| otherIterator != other.stackValue->crend(); |
| ++thisIterator, ++otherIterator) { |
| modified |= thisIterator->makeLeastUpperBound(*otherIterator); |
| } |
| |
| // If the other stack is higher, append the bottom of it to our current |
| // stack. |
| for (; otherIterator != other.stackValue->crend(); ++otherIterator) { |
| stackValue->push_front(*otherIterator); |
| modified = true; |
| } |
| |
| return modified; |
| } |
| |
| void print(std::ostream& os) { |
| if (isTop()) { |
| os << "TOP STACK" << std::endl; |
| return; |
| } |
| |
| for (auto iter = stackValue->rbegin(); iter != stackValue->rend(); |
| ++iter) { |
| iter->print(os); |
| os << std::endl; |
| } |
| } |
| |
| friend StackLattice; |
| }; |
| |
| // Like in computing the LUB, we compare the tops of the two stacks, as it |
| // handles the case of stacks of different scopes. Comparisons are done by |
| // element starting from the top. |
| // |
| // If the left stack is higher, and left top >= right top, then we say |
| // that left stack > right stack. If the left stack is lower and the left top |
| // >= right top or if the left stack is higher and the right top > left top or |
| // they are unrelated, then there is no relation. Same applies for the reverse |
| // relationship. |
| LatticeComparison compare(const Element& left, const Element& right) { |
| // Handle cases where there are top elements. |
| if (left.isTop()) { |
| if (right.isTop()) { |
| return LatticeComparison::EQUAL; |
| } else { |
| return LatticeComparison::GREATER; |
| } |
| } else if (right.isTop()) { |
| return LatticeComparison::LESS; |
| } |
| |
| bool hasLess = false; |
| bool hasGreater = false; |
| |
| // Check the tops of both stacks which match (i.e. are within the heights |
| // of both stacks). If there is a pair which is not related, the stacks |
| // cannot be related. |
| for (auto leftIterator = left.stackValue->crbegin(), |
| rightIterator = right.stackValue->crbegin(); |
| leftIterator != left.stackValue->crend() && |
| rightIterator != right.stackValue->crend(); |
| ++leftIterator, ++rightIterator) { |
| LatticeComparison currComparison = |
| stackElementLattice.compare(*leftIterator, *rightIterator); |
| switch (currComparison) { |
| case LatticeComparison::NO_RELATION: |
| return LatticeComparison::NO_RELATION; |
| case LatticeComparison::LESS: |
| hasLess = true; |
| break; |
| case LatticeComparison::GREATER: |
| hasGreater = true; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| // Check cases for when the stacks have unequal. As a rule, if a stack |
| // is higher, it is greater than the other stack, but if and only if |
| // when comparing their matching top portions the top portion of the |
| // higher stack is also >= the top portion of the shorter stack. |
| if (left.stackValue->size() > right.stackValue->size()) { |
| hasGreater = true; |
| } else if (right.stackValue->size() > left.stackValue->size()) { |
| hasLess = true; |
| } |
| |
| if (hasLess && !hasGreater) { |
| return LatticeComparison::LESS; |
| } else if (hasGreater && !hasLess) { |
| return LatticeComparison::GREATER; |
| } else if (hasLess && hasGreater) { |
| // Contradiction, and therefore must be unrelated. |
| return LatticeComparison::NO_RELATION; |
| } else { |
| return LatticeComparison::EQUAL; |
| } |
| } |
| |
| Element getBottom() { return Element{}; } |
| }; |
| |
| } // namespace wasm::analysis |
| |
| #endif // wasm_analysis_stack_lattice_h |