/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // The methods 'AvlTreeImpl::insert_worker' and 'AvlTreeImpl::delete_worker', // and all supporting methods reachable from them, are derived from a public // domain implementation by Georg Kraml. The public domain implementation in // C was translated into Rust and the Rust translation was later translated // into this C++ implementation. // // Unfortunately the relevant web site for the original C version is long // gone, and can only be found on the Wayback Machine: // // https://web.archive.org/web/20010419134337/ // http://www.kraml.at/georg/avltree/index.html // // https://web.archive.org/web/20030926063347/ // http://www.kraml.at/georg/avltree/avlmonolithic.c // // https://web.archive.org/web/20030401124003/http://www.kraml.at/src/howto/ // // The intermediate Rust translation can be found at // // https://github.com/bytecodealliance/regalloc.rs/blob/main/lib/src/avl_tree.rs // // For relicensing clearances, see Mozilla bugs 1620332 and 1769261: // // https://bugzilla.mozilla.org/show_bug.cgi?id=1620332 // https://bugzilla.mozilla.org/show_bug.cgi?id=1769261 // // All other code in this file originates from Mozilla. #ifndef ds_AvlTree_h #define ds_AvlTree_h #include "mozilla/Attributes.h" #include "mozilla/Likely.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/Maybe.h" #include "ds/LifoAlloc.h" namespace js { //////////////////////////////////////////////////////////////////////// // // // AvlTree implementation. For interface see `class AvlTree` below. // // // //////////////////////////////////////////////////////////////////////// // An AVL tree class, with private allocator and node-recycling. `T` is the // class of elements in the tree. `C` must provide a method // // static int compare(const T&, const T&) // // to provide a total ordering on values `T` that are put into the tree, // returning -1 for less-than, 0 for equal, and 1 for greater-than. // // `C::compare` does not have to be a total ordering for *all* values of `T`, // but it must be so for the `T` values in the tree. Requests to insert // duplicate `T` values, as determined equal by `C::compare`, are valid but // will be ignored in this implementation class: the stored data is unchanged. // The interface class `AvlTree` however will MOZ_CRASH() on such requests. // // `T` values stored in the tree will not be explicitly freed or destroyed. // // For best cache-friendlyness, try to put the fields of `T` that are read by // your compare function at the end of `T`. See comment on `struct Node` // below. // // Some operations require (internally) building a stack of tree nodes from // the root to some leaf. The maximum stack size, and hence the maximum tree // depth, is currently bounded at 48. The max depth of an AVL tree is roughly // 1.44 * log2(# nodes), so providing the tree-balancing machinery works // correctly, the max number of nodes is at least 2^(48 / 1.44), somewhat over // 2^33 (= 8 G). On a 32-bit target we'll run out of address space long // before reaching that. On a 64-bit target, the minimum imaginable // sizeof(Node) is 16 (for the two pointers), so a tree with 2^33 nodes would // occupy at least 2^37 bytes, viz, around 137GB. So this seems unlikely to // be a limitation. // // All stack-pushing operations are release-asserted to not overflow the stack. template class AvlTreeImpl { // This is the implementation of AVL trees. If you want to know how to use // them in your code, don't read this; instead look below at the public // interface, that is, `class AvlTree`. // // All of `AvlTreeImpl`, apart from the iterator code at the bottom, is // protected. Public facilities are provided by child class `AvlTree`. protected: // Tree node tags. enum class Tag { Free, // Node not in use -- is on the freelist. None, // Node in use. Neither subtree is deeper. Left, // Node in use. The left subtree is deeper. Right, // Node in use. The right subtree is deeper. Count, // Not used as an actual tag - should remain last in this list }; // Tree nodes. The tag is stored in the lower bits of the right Node pointer // rather than as a separate field. For types T with alignment no more than // twice the size of a pointer (ie, most types), this reduces the size of Node // and enables them to pack more tightly, reducing memory requirements and // improving cache behavior. (See bug 1847616.) struct Node { T item; Node* left; // This is the mask to use to extract the tag from `rightAndTag`. static constexpr uintptr_t kTagMask = 3; static_assert(mozilla::IsPowerOfTwo(kTagMask + 1), "kTagMask must only have a consecutive sequence of its " "lowest bits set"); static_assert( kTagMask >= static_cast(Tag::Count) - 1, "kTagMask must be sufficient to cover largest value in 'Tag'"); private: uintptr_t rightAndTag; public: explicit Node(const T& item) : item(item), left(nullptr), rightAndTag(static_cast(Tag::None)) {} [[nodiscard]] Node* getRight() const { return reinterpret_cast(rightAndTag & ~kTagMask); } [[nodiscard]] Tag getTag() const { return static_cast(rightAndTag & kTagMask); } void setRight(Node* right) { rightAndTag = reinterpret_cast(right) | static_cast(getTag()); } void setTag(const Tag tag) { rightAndTag = (rightAndTag & ~kTagMask) | static_cast(tag); } void setRightAndTag(Node* right, const Tag tag) { const uintptr_t rightAsUint = reinterpret_cast(right); rightAndTag = rightAsUint | static_cast(tag); } }; // Ensure that we can use the needed lower bits of a Node pointer to store the // tag. static_assert(alignof(Node) >= Node::kTagMask + 1); // Once-per-tree components. Node* root_; Node* freeList_; LifoAlloc* alloc_; // As a modest but easy optimisation, ::allocateNode will allocate one node // at the first call that sees an empty `freeList_`, two on the next such // call and four on subsequent such calls. This has the effect of reducing // the number of calls to the underlying allocator `alloc_` by a factor of 4 // for all but the smallest trees. It also helps pack more nodes into each // cache line. The limit of 4 exists for three reasons: // // (1) It gains the majority (75%) of the available benefit from reducing // the number of calls to `alloc_`, as the allocation size tends to // infinity. // // (2) Similarly, 4 `struct Node`s will surely be greater than 128 bytes, // hence there is minimal chance to use even fewer cache lines by increasing // the group size further. In any case most machines have cache lines of // size 64 bytes, not 128. // // (3) Most importantly, it limits the maximum potentially wasted space, // which is the case where a request causes an allocation of N nodes, of // which one is used immediately and the N-1 are put on the freelist, but // then -- because the tree never grows larger -- are never used. Given // that N=4 here, the worst case lossage is 3 nodes, which seems tolerable. uint32_t nextAllocSize_; // 1, 2 or 4 only // The expected maximum tree depth. See comments above. static const size_t MAX_TREE_DEPTH = 48; AvlTreeImpl(const AvlTreeImpl&) = delete; AvlTreeImpl& operator=(const AvlTreeImpl&) = delete; // ---- Preliminaries --------------------------------------- // explicit AvlTreeImpl(LifoAlloc* alloc = nullptr) : root_(nullptr), freeList_(nullptr), alloc_(alloc), nextAllocSize_(1) {} void setAllocator(LifoAlloc* alloc) { alloc_ = alloc; } // Put `node` onto the free list, for possible later reuse. inline void addToFreeList(Node* node) { node->left = freeList_; node->setRightAndTag(nullptr, Tag::Free); // for safety freeList_ = node; } // A safer version of `addToFreeList`. inline void freeNode(Node* node) { MOZ_ASSERT(node->getTag() != Tag::Free); addToFreeList(node); } // This is the slow path for ::allocateNode below. Allocate 1, 2 or 4 nodes // as a block, return the first one properly initialised, and put the rest // on the freelist, in increasing order of address. MOZ_NEVER_INLINE Node* allocateNodeOOL(const T& v) { switch (nextAllocSize_) { case 1: { nextAllocSize_ = 2; Node* node = alloc_->new_(v); // `node` is either fully initialized, or nullptr on OOM. return node; } case 2: { nextAllocSize_ = 4; Node* nodes = alloc_->newArrayUninitialized(2); if (!nodes) { return nullptr; } Node* node0 = &nodes[0]; addToFreeList(&nodes[1]); new (node0) Node(v); return node0; } case 4: { Node* nodes = alloc_->newArrayUninitialized(4); if (!nodes) { return nullptr; } Node* node0 = &nodes[0]; addToFreeList(&nodes[3]); addToFreeList(&nodes[2]); addToFreeList(&nodes[1]); new (node0) Node(v); return node0; } default: { MOZ_CRASH(); } } } // Allocate a Node holding `v`, or return nullptr on OOM. All of the fields // are initialized. inline Node* allocateNode(const T& v) { Node* node = freeList_; if (MOZ_LIKELY(node)) { MOZ_ASSERT(node->getTag() == Tag::Free); freeList_ = node->left; new (node) Node(v); return node; } return allocateNodeOOL(v); } // These exist only transiently, to aid rebalancing. They indicate whether // an insertion/deletion succeeded, whether subsequent rebalancing is // needed. enum class Result { Error, OK, Balance }; using NodeAndResult = std::pair; // Standard AVL single-rotate-left Node* rotate_left(Node* old_root) { Node* new_root = old_root->getRight(); old_root->setRight(new_root->left); new_root->left = old_root; return new_root; } // Standard AVL single-rotate-right Node* rotate_right(Node* old_root) { Node* new_root = old_root->left; old_root->left = new_root->getRight(); new_root->setRight(old_root); return new_root; } // ---- Helpers for insertion ------------------------------- // // `leftgrown`: a helper function for `insert_worker` // // Parameters: // // root Root of a tree. This node's left subtree has just grown due to // item insertion; its "tag" flag needs adjustment, and the local // tree (the subtree of which this node is the root node) may have // become unbalanced. // // Return values: // // The new root of the subtree, plus either: // // OK The local tree could be rebalanced or was balanced from the // start. The caller, insert_worker, may assume the entire tree // is valid. // or // Balance The local tree was balanced, but has grown in height. // Do not assume the entire tree is valid. // // This function has been split into two pieces: `leftgrown`, which is small // and hot, and is marked always-inline, and `leftgrown_left`, which handles // a more complex and less frequent case, and is marked never-inline. The // intent is to have the common case always inlined without having to deal // with the extra register pressure from inlining the less frequent code. // The dual function `rightgrown` is split similarly. MOZ_NEVER_INLINE Node* leftgrown_left(Node* root) { if (root->left->getTag() == Tag::Left) { root->setTag(Tag::None); root->left->setTag(Tag::None); root = rotate_right(root); } else { switch (root->left->getRight()->getTag()) { case Tag::Left: root->setTag(Tag::Right); root->left->setTag(Tag::None); break; case Tag::Right: root->setTag(Tag::None); root->left->setTag(Tag::Left); break; case Tag::None: root->setTag(Tag::None); root->left->setTag(Tag::None); break; case Tag::Free: default: MOZ_CRASH(); } root->left->getRight()->setTag(Tag::None); root->left = rotate_left(root->left); root = rotate_right(root); } return root; } inline NodeAndResult leftgrown(Node* root) { switch (root->getTag()) { case Tag::Left: return NodeAndResult(leftgrown_left(root), Result::OK); case Tag::Right: root->setTag(Tag::None); return NodeAndResult(root, Result::OK); case Tag::None: root->setTag(Tag::Left); return NodeAndResult(root, Result::Balance); case Tag::Free: default: break; } MOZ_CRASH(); } // `rightgrown`: a helper function for `insert_worker`. See `leftgrown` for // details. MOZ_NEVER_INLINE Node* rightgrown_right(Node* root) { if (root->getRight()->getTag() == Tag::Right) { root->setTag(Tag::None); root->getRight()->setTag(Tag::None); root = rotate_left(root); } else { switch (root->getRight()->left->getTag()) { case Tag::Right: root->setTag(Tag::Left); root->getRight()->setTag(Tag::None); break; case Tag::Left: root->setTag(Tag::None); root->getRight()->setTag(Tag::Right); break; case Tag::None: root->setTag(Tag::None); root->getRight()->setTag(Tag::None); break; case Tag::Free: default: MOZ_CRASH(); } root->getRight()->left->setTag(Tag::None); root->setRight(rotate_right(root->getRight())); root = rotate_left(root); } return root; } inline NodeAndResult rightgrown(Node* root) { switch (root->getTag()) { case Tag::Left: root->setTag(Tag::None); return NodeAndResult(root, Result::OK); case Tag::Right: return NodeAndResult(rightgrown_right(root), Result::OK); case Tag::None: root->setTag(Tag::Right); return NodeAndResult(root, Result::Balance); case Tag::Free: default: break; } MOZ_CRASH(); } // ---- Insertion ------------------------------------------- // // Worker for insertion. Allocates a node for `v` and inserts it into the // tree. Returns: nullptr for OOM; (Node*)1 if `v` is a duplicate (per // `C::compare`), in which case the tree is unchanged; otherwise (successful // insertion) the new root. In the latter case, the new `item` field is // initialised from `v`. Node* insert_worker(const T& v) { // Insertion is a two pass process. In the first pass, we descend from // the root, looking for the place in the tree where the new node will go, // and at the same time storing the sequence of visited nodes in a stack. // In the second phase we re-ascend the tree, as guided by the stack, // rebalancing as we go. // // Note, we start from `root_`, but that isn't updated at the end. Instead // the new value is returned to the caller, which has to do the update. Node* stack[MAX_TREE_DEPTH]; size_t stackPtr = 0; // points to next available slot #define STACK_ENTRY_SET_IS_LEFT(_nodePtr) \ ((Node*)(uintptr_t(_nodePtr) | uintptr_t(1))) #define STACK_ENTRY_GET_IS_LEFT(_ent) ((bool)(uintptr_t(_ent) & uintptr_t(1))) #define STACK_ENTRY_GET_NODE(_ent) ((Node*)(uintptr_t(_ent) & ~uintptr_t(1))) // In the first phase, walk down the tree to find the place where the new // node should be inserted, recording our path in `stack`. This loop has // a factor-of-2 unrolling (the loop body contains two logical iterations) // in order to reduce the overall cost of the stack-overflow check at the // bottom. Node* node = root_; while (true) { // First logical iteration if (!node) { break; } int cmpRes1 = C::compare(v, node->item); if (cmpRes1 < 0) { stack[stackPtr++] = STACK_ENTRY_SET_IS_LEFT(node); node = node->left; } else if (cmpRes1 > 0) { stack[stackPtr++] = node; node = node->getRight(); } else { // `v` is already in the tree. Inform the caller, and don't change // the tree. return (Node*)(uintptr_t(1)); } // Second logical iteration if (!node) { break; } int cmpRes2 = C::compare(v, node->item); if (cmpRes2 < 0) { stack[stackPtr++] = STACK_ENTRY_SET_IS_LEFT(node); node = node->left; } else if (cmpRes2 > 0) { stack[stackPtr++] = node; node = node->getRight(); } else { return (Node*)(uintptr_t(1)); } // We're going around again. Ensure there are at least two available // stack slots. MOZ_RELEASE_ASSERT(stackPtr < MAX_TREE_DEPTH - 2); } MOZ_ASSERT(!node); // Now allocate the new node. Node* new_node = allocateNode(v); if (!new_node) { return nullptr; // OOM } // And unwind the stack, back to the root, rebalancing as we go. Once get // to a place where the new subtree doesn't need to be rebalanced, we can // stop this upward scan, because no nodes above it will need to be // rebalanced either. Node* curr_node = new_node; Result curr_node_action = Result::Balance; while (stackPtr > 0) { Node* parent_node_tagged = stack[--stackPtr]; Node* parent_node = STACK_ENTRY_GET_NODE(parent_node_tagged); if (STACK_ENTRY_GET_IS_LEFT(parent_node_tagged)) { parent_node->left = curr_node; if (curr_node_action == Result::Balance) { auto pair = leftgrown(parent_node); curr_node = pair.first; curr_node_action = pair.second; } else { curr_node = parent_node; break; } } else { parent_node->setRight(curr_node); if (curr_node_action == Result::Balance) { auto pair = rightgrown(parent_node); curr_node = pair.first; curr_node_action = pair.second; } else { curr_node = parent_node; break; } } } if (stackPtr > 0) { curr_node = STACK_ENTRY_GET_NODE(stack[0]); } MOZ_ASSERT(curr_node); #undef STACK_ENTRY_SET_IS_LEFT #undef STACK_ENTRY_GET_IS_LEFT #undef STACK_ENTRY_GET_NODE return curr_node; } // ---- Helpers for deletion -------------------------------- // // `leftshrunk`: a helper function for `delete_worker` and `findlowest` // // Parameters: // // n Pointer to a node. The node's left subtree has just shrunk due to // item removal; its "skew" flag needs adjustment, and the local tree // (the subtree of which this node is the root node) may have become // unbalanced. // // Return values: // // (jseward: apparently some node, but what is it?), plus either: // // OK The parent activation of the delete activation that called // this function may assume the entire tree is valid. // // Balance Do not assume the entire tree is valid. NodeAndResult leftshrunk(Node* n) { switch (n->getTag()) { case Tag::Left: { n->setTag(Tag::None); return NodeAndResult(n, Result::Balance); } case Tag::Right: { if (n->getRight()->getTag() == Tag::Right) { n->setTag(Tag::None); n->getRight()->setTag(Tag::None); n = rotate_left(n); return NodeAndResult(n, Result::Balance); } else if (n->getRight()->getTag() == Tag::None) { n->setTag(Tag::Right); n->getRight()->setTag(Tag::Left); n = rotate_left(n); return NodeAndResult(n, Result::OK); } else { switch (n->getRight()->left->getTag()) { case Tag::Left: n->setTag(Tag::None); n->getRight()->setTag(Tag::Right); break; case Tag::Right: n->setTag(Tag::Left); n->getRight()->setTag(Tag::None); break; case Tag::None: n->setTag(Tag::None); n->getRight()->setTag(Tag::None); break; case Tag::Free: default: MOZ_CRASH(); } n->getRight()->left->setTag(Tag::None); n->setRight(rotate_right(n->getRight())); ; n = rotate_left(n); return NodeAndResult(n, Result::Balance); } /*NOTREACHED*/ MOZ_CRASH(); } case Tag::None: { n->setTag(Tag::Right); return NodeAndResult(n, Result::OK); } case Tag::Free: default: { MOZ_CRASH(); } } MOZ_CRASH(); } // rightshrunk: a helper function for `delete` and `findhighest`. See // `leftshrunk` for details. NodeAndResult rightshrunk(Node* n) { switch (n->getTag()) { case Tag::Right: { n->setTag(Tag::None); return NodeAndResult(n, Result::Balance); } case Tag::Left: { if (n->left->getTag() == Tag::Left) { n->setTag(Tag::None); n->left->setTag(Tag::None); n = rotate_right(n); return NodeAndResult(n, Result::Balance); } else if (n->left->getTag() == Tag::None) { n->setTag(Tag::Left); n->left->setTag(Tag::Right); n = rotate_right(n); return NodeAndResult(n, Result::OK); } else { switch (n->left->getRight()->getTag()) { case Tag::Left: n->setTag(Tag::Right); n->left->setTag(Tag::None); break; case Tag::Right: n->setTag(Tag::None); n->left->setTag(Tag::Left); break; case Tag::None: n->setTag(Tag::None); n->left->setTag(Tag::None); break; case Tag::Free: default: MOZ_CRASH(); } n->left->getRight()->setTag(Tag::None); n->left = rotate_left(n->left); n = rotate_right(n); return NodeAndResult(n, Result::Balance); } /*NOTREACHED*/ MOZ_CRASH(); } case Tag::None: { n->setTag(Tag::Left); return NodeAndResult(n, Result::OK); } case Tag::Free: default: { MOZ_CRASH(); } } MOZ_CRASH(); } // `findhighest`: helper function for `delete_worker`. It replaces a node // with a subtree's greatest (per C::compare) item. // // Parameters: // // target Pointer to node to be replaced. // // n Address of pointer to subtree. // // Return value: // // Nothing The target node could not be replaced because the subtree // provided was empty. // // Some(Node*,Result) jseward: it's pretty unclear, but I *think* it // is a pair that has the same meaning as the // pair returned by `leftgrown`, as described above. mozilla::Maybe findhighest(Node* target, Node* n) { if (n == nullptr) { return mozilla::Nothing(); } auto res = Result::Balance; if (n->getRight() != nullptr) { auto fhi = findhighest(target, n->getRight()); if (fhi.isSome()) { n->setRight(fhi.value().first); res = fhi.value().second; if (res == Result::Balance) { auto pair = rightshrunk(n); n = pair.first; res = pair.second; } return mozilla::Some(NodeAndResult(n, res)); } else { return mozilla::Nothing(); } } target->item = n->item; Node* tmp = n; n = n->left; freeNode(tmp); return mozilla::Some(NodeAndResult(n, res)); } // `findhighest`: helper function for `delete_worker`. It replaces a node // with a subtree's greatest (per C::compare) item. See `findhighest` for // details. mozilla::Maybe findlowest(Node* target, Node* n) { if (n == nullptr) { return mozilla::Nothing(); } Result res = Result::Balance; if (n->left != nullptr) { auto flo = findlowest(target, n->left); if (flo.isSome()) { n->left = flo.value().first; res = flo.value().second; if (res == Result::Balance) { auto pair = leftshrunk(n); n = pair.first; res = pair.second; } return mozilla::Some(NodeAndResult(n, res)); } else { return mozilla::Nothing(); } } target->item = n->item; Node* tmp = n; n = n->getRight(); freeNode(tmp); return mozilla::Some(NodeAndResult(n, res)); } // ---- Deletion -------------------------------------------- // // Deletes the node matching `item` from an arbitrary subtree rooted at // `node`. Returns the root of the new subtree (if any), a `Result` that // indicates that either, the tree containing `node` does or does not need // rebalancing (::Balance, ::OK) or that the item was not found (::Error). NodeAndResult delete_worker(Node* node, const T& item) { Result tmp = Result::Balance; if (node == nullptr) { return NodeAndResult(node, Result::Error); } int cmp_res = C::compare(item, node->item); if (cmp_res < 0) { auto pair1 = delete_worker(node->left, item); node->left = pair1.first; tmp = pair1.second; if (tmp == Result::Balance) { auto pair2 = leftshrunk(node); node = pair2.first; tmp = pair2.second; } return NodeAndResult(node, tmp); } else if (cmp_res > 0) { auto pair1 = delete_worker(node->getRight(), item); node->setRight(pair1.first); tmp = pair1.second; if (tmp == Result::Balance) { auto pair2 = rightshrunk(node); node = pair2.first; tmp = pair2.second; } return NodeAndResult(node, tmp); } else { if (node->left != nullptr) { auto fhi = findhighest(node, node->left); if (fhi.isSome()) { node->left = fhi.value().first; tmp = fhi.value().second; if (tmp == Result::Balance) { auto pair = leftshrunk(node); node = pair.first; tmp = pair.second; } } return NodeAndResult(node, tmp); } if (node->getRight() != nullptr) { auto flo = findlowest(node, node->getRight()); if (flo.isSome()) { node->setRight(flo.value().first); tmp = flo.value().second; if (tmp == Result::Balance) { auto pair = rightshrunk(node); node = pair.first; tmp = pair.second; } } return NodeAndResult(node, tmp); } freeNode(node); return NodeAndResult(nullptr, Result::Balance); } } // ---- Lookup ---------------------------------------------- // // Find the node matching `v`, or return nullptr if not found. Node* find_worker(const T& v) const { Node* node = root_; while (node) { int cmpRes = C::compare(v, node->item); if (cmpRes < 0) { node = node->left; } else if (cmpRes > 0) { node = node->getRight(); } else { return node; } } return nullptr; } // ---- Iteration ------------------------------------------- // public: // This provides iteration forwards over the tree. You can either iterate // over the whole tree or specify a start point. To iterate over the whole // tree: // // AvlTree tree; // .. put stuff into `tree` .. // // AvlTree::Iter iter(&tree); // while (iter.hasMore) { // MyT item = iter.next(); // } // // Alternatively you can initialize the iterator with some value `startAt`, // so that the first value you get is greater than or equal to `startAt`, // per `MyC::compare`: // // AvlTree::Iter iter(&tree, startAt); // // Starting the iterator at a particular value requires finding the value in // the tree and recording the path to it. So it's nearly as cheap as a call // to `AvlTree::contains` and you can use it as a plausible substitute for // `::contains` if you want. // // Note that `class Iter` is quite large -- around 50 machine words -- so // you might want to think twice before allocating instances on the heap. class Iter { const AvlTreeImpl* tree_; Node* stack_[MAX_TREE_DEPTH]; size_t stackPtr_; // This sets up the iterator stack so that the first value it produces // will be the smallest value that is greater than or equal to `v`. Note // the structural similarity to ::find_worker above. // // The logic for pushing nodes on the stack looks strange at first. Once // set up, the stack contains a root-to-some-node path, and the // top-of-stack value is the next value the iterator will emit. If the // stack becomes empty then the iteration is complete. // // It's not quite accurate to say that the stack contains a complete // root-to-some-node path. Rather, the stack contains such a path, except // it omits nodes at which the path goes to the right child. Eg: // // 5 // 3 8 // 1 4 7 9 // // If the next item to be emitted is 4, then the stack will be [5, 4] and // not [5, 3, 4], because at 3 we go right. This explains why the // `cmpRes > 0` case in `setupIteratorStack` doesn't push an item on the // stack. It also explains why the single-argument `Iter::Iter` below, // which sets up for iteration starting at the lowest element, simply // calls `visitLeftChildren` to do its work. void setupIteratorStack(Node* node, const T& v) { // Ensure stackPtr_ is cached in a register, since this function can be // hot. MOZ_ASSERT(stackPtr_ == 0); size_t stackPtr = 0; while (node) { int cmpRes = C::compare(v, node->item); if (cmpRes < 0) { stack_[stackPtr++] = node; MOZ_RELEASE_ASSERT(stackPtr < MAX_TREE_DEPTH); node = node->left; } else if (cmpRes > 0) { node = node->getRight(); } else { stack_[stackPtr++] = node; MOZ_RELEASE_ASSERT(stackPtr < MAX_TREE_DEPTH); break; } } stackPtr_ = stackPtr; } void visitLeftChildren(Node* node) { while (true) { Node* left = node->left; if (left == nullptr) { break; } stack_[stackPtr_++] = left; MOZ_RELEASE_ASSERT(stackPtr_ < MAX_TREE_DEPTH); node = left; } } public: explicit Iter(const AvlTreeImpl* tree) { tree_ = tree; stackPtr_ = 0; if (tree->root_ != nullptr) { stack_[stackPtr_++] = tree->root_; MOZ_RELEASE_ASSERT(stackPtr_ < MAX_TREE_DEPTH); visitLeftChildren(tree->root_); } } Iter(const AvlTreeImpl* tree, const T& startAt) { tree_ = tree; stackPtr_ = 0; setupIteratorStack(tree_->root_, startAt); } bool hasMore() const { return stackPtr_ > 0; } T next() { MOZ_RELEASE_ASSERT(stackPtr_ > 0); Node* ret = stack_[--stackPtr_]; Node* right = ret->getRight(); if (right != nullptr) { stack_[stackPtr_++] = right; MOZ_RELEASE_ASSERT(stackPtr_ < MAX_TREE_DEPTH); visitLeftChildren(right); } return ret->item; } }; }; //////////////////////////////////////////////////////////////////////// // // // AvlTree public interface, for SpiderMonkey. // // // //////////////////////////////////////////////////////////////////////// // This public interface is fairly limited and restrictive. If you need to // add more functionality, consider copying code from `class AvlTreeTestIF` in // js/src/jsapi-tests/testAvlTree.cpp rather than rolling your own. See // comments there. template class AvlTree : public AvlTreeImpl { // Shorthands for names in the implementation (parent) class. using Impl = AvlTreeImpl; using ImplNode = typename AvlTreeImpl::Node; using ImplResult = typename AvlTreeImpl::Result; using ImplNodeAndResult = typename AvlTreeImpl::NodeAndResult; public: explicit AvlTree(LifoAlloc* alloc = nullptr) : Impl(alloc) {} // You'll need to tell the tree how to allocate nodes, either here or in // `AvlTree::AvlTree`. void setAllocator(LifoAlloc* alloc) { Impl::setAllocator(alloc); } // Is the tree empty? bool empty() const { return Impl::root_ == nullptr; } // Insert `v` in the tree. Returns false to indicate OOM. `v` may not be // equal to any existing value in the tree, per `C::compare`; if it is, this // routine will MOZ_CRASH(). It would be trivial to change this to replace // an existing value instead, if needed. [[nodiscard]] bool insert(const T& v) { ImplNode* new_root = Impl::insert_worker(v); // Take out both unlikely cases with a single comparison. if (MOZ_UNLIKELY(uintptr_t(new_root) <= uintptr_t(1))) { // OOM (new_root == 0) or duplicate (new_root == 1) if (!new_root) { // OOM return false; } // Duplicate; tree is unchanged. MOZ_CRASH(); } Impl::root_ = new_root; return true; } // Remove `v` from the tree. `v` must actually be in the tree, per // `C::compare`. If it is not, this routine will MOZ_CRASH(). // Superficially it looks like we could change it to return without doing // anything in that case, if needed, except we'd need to first verify that // `delete_worker` doesn't change the tree in that case. void remove(const T& v) { ImplNodeAndResult pair = Impl::delete_worker(Impl::root_, v); ImplNode* new_root = pair.first; ImplResult res = pair.second; if (MOZ_UNLIKELY(res == ImplResult::Error)) { // `v` isn't in the tree. MOZ_CRASH(); } else { Impl::root_ = new_root; } } // Determine whether the tree contains `v` and if so return, in `res`, a // copy of the stored version. Note that the determination is done using // `C::compare` and you should consider carefully the consequences of // passing in `v` for which `C::compare` indicates "equal" for more than one // value in the tree. This is not invalid, but it does mean that you may be // returned, via `res`, *any* of the values in the tree that `compare` deems // equal to `v`, and which you get is arbitrary -- it depends on which is // closest to the root. bool contains(const T& v, T* res) const { ImplNode* node = Impl::find_worker(v); if (node) { *res = node->item; return true; } return false; } // Determine whether the tree contains `v` and if so return the address of // the stored version. The comments on `::contains` about the meaning of // `C::compare` apply here too. T* maybeLookup(const T& v) { ImplNode* node = Impl::find_worker(v); if (node) { return &(node->item); } return nullptr; } // AvlTree::Iter is also public; it's just pass-through from AvlTreeImpl. // See documentation above on AvlTree::Iter on how to use it. }; } /* namespace js */ #endif /* ds_AvlTree_h */