#ifndef wasm_analysis_stack_lattice_h #define wasm_analysis_stack_lattice_h #include #include #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 class StackLattice { static_assert(is_lattice); 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> stackValue = std::deque(); 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