All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.rhino.HamtPMap Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Rhino code, released
 * May 6, 1999.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Google Inc.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package com.google.javascript.rhino;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.javascript.rhino.PMap.Reconciler;
import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.BiPredicate;
import org.jspecify.nullness.Nullable;

/**
 * An immutable sorted map with efficient (persistent) updates.
 *
 * 

Uses a hash array mapped trie: http://en.wikipedia.org/wiki/Hash_array_mapped_trie. * *

This implementation stores the bare minimum in each node: a key (with its hash), value, mask, * and children. It is also optimized to take maximum advantage of binary operations on shared * trees. Specifically, {@link #reconcile} avoids recursing into entire subtrees if they are * identical objects. Null keys and values are not allowed. The implementation special-cases away * the EMPTY map as soon as possible, using 'null' instead for all the logic (since EMPTY violates * the invariant that key and value are non-null). Finally, we maintain an invariant that the * entry with the smallest hash code is always at the root of the tree, which avoids almost all * extra tree rebuilding during binary operations. */ // TODO(sdh): Consider using tricks from https://bendyworks.com/blog/leveling-clojures-hash-maps // We need a solid way to profile the results to see if it's actually worth the extra code. public final class HamtPMap implements PMap, Serializable { /** * Number of bits of fan-out at each level. May be anywhere from 1 (a binary tree) to 5 (for a * fan-out of 32). Anything larger requires using a long for the mask (which is ugly for the * JavaScript-compiled version), and zero is essentially a linked list. The relevant trade-off is * that more fan-out means shallower trees, which should lead to quicker look-up times and more * efficient updates, but less fan-out makes reconcile() and equivalent() more efficient for maps * that are mostly the same, since bigger common trees can be pruned from the operation. * Preliminary profiling suggests that for type-checking purposes, all nonzero values are roughly * comparable. */ private static final int BITS = 4; /** Number of bits to shift off to get the most significant BITS number of bits. */ private static final int BITS_SHIFT = 32 - BITS; /** Non-null key (exception: empty map has a null key). */ private final K key; /** Hash of the key, right-shifted by BITS*depth. */ private final int hash; /** Non-null value (exceptions: (1) empty map, (2) result of pivot, if not found). */ private final V value; /** Bit mask indicating the children that are present (bitCount(mask) == children.length). */ private final int mask; /** Non-null array of children. Elements are never reassigned. */ private final HamtPMap[] children; private static final HamtPMap[] EMPTY_CHILDREN = new HamtPMap[0]; private static final HamtPMap EMPTY = new HamtPMap<>(null, 0, null, 0, emptyChildren()); private HamtPMap(K key, int hash, V value, int mask, HamtPMap[] children) { this.key = key; this.hash = hash; this.value = value; this.mask = mask; this.children = children; this.checkInvariants(); } private void checkInvariants() { checkState(Integer.bitCount(this.mask) == this.children.length); for (HamtPMap child : this.children) { checkNotNull(child); } } /** Returns an empty map. */ @SuppressWarnings("unchecked") // Empty immutable collection is safe to cast. @CheckReturnValue public static HamtPMap empty() { return (HamtPMap) EMPTY; } /** Returns an empty array of child maps. */ @SuppressWarnings("unchecked") // Empty array is safe to cast. private static HamtPMap[] emptyChildren() { return (HamtPMap[]) EMPTY_CHILDREN; } @Override @CheckReturnValue public String toString() { StringBuilder sb = new StringBuilder().append("{"); if (!isEmpty()) { appendTo(sb); } return sb.append("}").toString(); } /** Appends this map's contents to a string builder. */ private void appendTo(StringBuilder sb) { if (sb.length() > 1) { sb.append(", "); } sb.append(key).append(": ").append(value); for (HamtPMap child : children) { child.appendTo(sb); } } /** Returns whether this map is empty. */ @Override @CheckReturnValue public boolean isEmpty() { return key == null; } /** Returns an iterable for a (possibly null) tree. */ @Override @CheckReturnValue public Iterable values() { if (isEmpty()) { return Collections.emptyList(); } return () -> new Iter<>(this, map -> map.value); } /** Returns an iterable for a (possibly null) tree. */ @Override @CheckReturnValue public Iterable keys() { if (isEmpty()) { return Collections.emptyList(); } return () -> new Iter<>(this, map -> map.key); } /** * Retrieves the value associated with the given key from the map, or returns null if it is not * present. */ @Override @CheckReturnValue public V get(K key) { return !isEmpty() ? get(key, hash(key)) : null; } /** Internal recursive implementation of get(K). */ private V get(K key, int hash) { if (hash == this.hash && key.equals(this.key)) { return this.value; } int bucket = bucket(hash); int bucketMask = 1 << bucket; return (mask & bucketMask) != 0 ? children[index(bucketMask)].get(key, shift(hash)) : null; } /** * Returns a new map with the given key-value pair added. If the value is already present, then * this same map will be returned. */ @Override @CheckReturnValue public HamtPMap plus(K key, V value) { checkNotNull(value); return !isEmpty() ? plus(key, hash(key), value) : new HamtPMap<>(key, hash(key), value, 0, emptyChildren()); } /** Internal recursive implementation of plus(K, V). */ private HamtPMap plus(K key, int hash, V value) { if (hash == this.hash && key.equals(this.key)) { return value.equals(this.value) ? this : new HamtPMap<>(key, hash, value, mask, children); } if (compareUnsigned(hash, this.hash) < 0) { return replaceRoot(key, hash, value); } int bucket = bucket(hash); hash = shift(hash); int bucketMask = 1 << bucket; int index = index(bucketMask); if ((mask & bucketMask) != 0) { // already a child, so overwrite HamtPMap child = children[index]; HamtPMap newChild = child.plus(key, hash, value); return child == newChild ? this : withChildren(mask, replaceChild(children, index, newChild)); } else { // insert at index HamtPMap newChild = new HamtPMap<>(key, hash, value, 0, emptyChildren()); return withChildren(mask | bucketMask, insertChild(children, index, newChild)); } } private HamtPMap replaceRoot(K key, int hash, V value) { int bucket = bucket(this.hash); int leafHash = shift(this.hash); int bucketMask = 1 << bucket; int index = index(bucketMask); HamtPMap[] newChildren; if ((mask & bucketMask) != 0) { newChildren = replaceChild(children, index, children[index].plus(this.key, leafHash, this.value)); } else { HamtPMap newChild = new HamtPMap<>(this.key, leafHash, this.value, 0, emptyChildren()); newChildren = insertChild(children, index, newChild); } return new HamtPMap<>(key, hash, value, mask | bucketMask, newChildren); } /** * Returns a new map with the given key removed. If the key was not present in the first place, * then this same map will be returned. */ @Override @CheckReturnValue public HamtPMap minus(K key) { return !isEmpty() ? minus(key, hash(key), null) : this; } /** * Internal recursive implementation of minus(K). The value of the removed node is returned via * the 'value' array, if it is non-null. */ private HamtPMap minus(K key, int hash, V @Nullable [] value) { if (hash == this.hash && key.equals(this.key)) { HamtPMap result = deleteRoot(mask, children); if (value != null) { value[0] = this.value; } return result != null ? result : empty(); } int bucket = bucket(hash); int bucketMask = 1 << bucket; if ((mask & bucketMask) == 0) { // not present, stop looking return this; } hash = shift(hash); int index = index(bucketMask); HamtPMap child = children[index]; HamtPMap newChild = child.minus(key, hash, value); if (newChild == child) { return this; } else if (newChild == EMPTY) { return withChildren(mask & ~bucketMask, deleteChild(children, index)); } else { return withChildren(mask, replaceChild(children, index, newChild)); } } @Override @CheckReturnValue public HamtPMap reconcile(PMap that, Reconciler joiner) { HamtPMap result = reconcile( !this.isEmpty() ? this : null, !that.isEmpty() ? (HamtPMap) that : null, (k, v1, v2) -> checkNotNull(joiner.merge(k, v1, v2))); return result != null ? result : empty(); } /** Internal recursive implementation of reconcile(HamtPMap, BiFunction), factoring out empies. */ private static HamtPMap reconcile( @Nullable HamtPMap t1, @Nullable HamtPMap t2, Reconciler joiner) { if (t1 == t2) { return t1; } else if (t1 == null) { V newValue = joiner.merge(t2.key, null, t2.value); HamtPMap[] newChildren = Arrays.copyOf(t2.children, t2.children.length); for (int i = 0; i < newChildren.length; i++) { newChildren[i] = reconcile(null, newChildren[i], joiner); } return newValue != null ? new HamtPMap<>(t2.key, t2.hash, newValue, t2.mask, newChildren) : deleteRoot(t2.mask, newChildren); } else if (t2 == null) { V newValue = joiner.merge(t1.key, t1.value, null); HamtPMap[] newChildren = Arrays.copyOf(t1.children, t1.children.length); for (int i = 0; i < newChildren.length; i++) { newChildren[i] = reconcile(newChildren[i], null, joiner); } return newValue != null ? new HamtPMap<>(t1.key, t1.hash, newValue, t1.mask, newChildren) : deleteRoot(t1.mask, newChildren); } // Try as hard as possible to return input trees exactly. boolean sameChildrenAs1 = true; boolean sameChildrenAs2 = true; // If the hashes are different, we need to keep the lower one at the top. int hashCmp = compareUnsigned(t1.hash, t2.hash); K key = t1.key; int hash = t1.hash; if (hashCmp < 0) { // t1.key is missing from t2 t2 = t2.vacateRoot(); sameChildrenAs2 = false; } else if (hashCmp > 0) { // t2.key is missing from t1 t1 = t1.vacateRoot(); sameChildrenAs1 = false; key = t2.key; hash = t2.hash; } else if (!t1.key.equals(t2.key)) { // Hash collision: try to rearrange t2 to have the same root as t1 t2 = t2.pivot(t1.key, t1.hash); sameChildrenAs2 = false; } // Note: one or the other (but not both) tree may have a null key at root. V newValue = Objects.equals(t1.value, t2.value) ? t1.value : joiner.merge(t1.key != null ? t1.key : t2.key, t1.value, t2.value); int newMask = t1.mask | t2.mask; sameChildrenAs1 &= (newMask == t1.mask); sameChildrenAs2 &= (newMask == t2.mask); @SuppressWarnings("unchecked") // only used internally. HamtPMap[] newChildren = newMask != 0 ? (HamtPMap[]) new HamtPMap[Integer.bitCount(newMask)] : emptyChildren(); int mask = newMask; int index = 0; while (mask != 0) { int childBit = Integer.lowestOneBit(mask); mask &= ~childBit; HamtPMap child1 = t1.getChild(childBit); HamtPMap child2 = t2.getChild(childBit); newChildren[index] = reconcile(child1, child2, joiner); sameChildrenAs1 &= (newChildren[index] == child1); sameChildrenAs2 &= (newChildren[index] == child2); if (newChildren[index] != null) { index++; } else { newMask &= ~childBit; } } if (sameChildrenAs1 && t1.value.equals(newValue)) { return t1; } else if (sameChildrenAs2 && t2.value.equals(newValue)) { return t2; } else if (newValue == null) { return deleteRoot(newMask, newChildren); } return new HamtPMap<>(key, hash, newValue, newMask, newChildren); } /** * Checks equality recursively based on the given equivalence. Short-circuits as soon as a 'false' * result is found. */ @Override public boolean equivalent(PMap that, BiPredicate equivalence) { return equivalent( !this.isEmpty() ? this : null, !that.isEmpty() ? (HamtPMap) that : null, equivalence); } /** Internal recursive implementation of equivalent(HamtPMap, BiPredicate). */ private static boolean equivalent( @Nullable HamtPMap t1, @Nullable HamtPMap t2, BiPredicate equivalence) { if (t1 == t2) { return true; } else if (t1 == null || t2 == null) { return false; } if (t1.hash != t2.hash) { // Due to our invariant, we can safely conclude that there's a discrepancy in the // keys without any extra work. return false; } else if (!t1.key.equals(t2.key)) { // Hash collision: try to rearrange t2 to have the same root as t1 t2 = t2.pivot(t1.key, t1.hash); if (t2.key == null) { // t1.key not found in t2 return false; } } if (!equivalence.test(t1.value, t2.value)) { return false; } int mask = t1.mask | t2.mask; while (mask != 0) { int childBit = Integer.lowestOneBit(mask); mask &= ~childBit; if (!equivalent(t1.getChild(childBit), t2.getChild(childBit), equivalence)) { return false; } } return true; } /** * Returns the index into the 'children' array for the given bit, which must have exactly one bit * set in its binary representation (i.e. must be a power of two). */ private int index(int bit) { return Integer.bitCount(mask & (bit - 1)); } /** * Returns the child for the given bit, which must have exactly one bit set. Returns null if there * is no child for that bit. */ private @Nullable HamtPMap getChild(int bit) { return (mask & bit) != 0 ? children[index(bit)] : null; } /** * Perform the hash operation. This is done once per object as it enters the map, then never * again since the result is passed around internally. We reverse the bits since certain types * (such as small integers and short strings) have hash codes that only vary in the least * significant bits, but we use the most significant bits for bucketing in order to maintain * a canonical structure where the key with the smallest hash is always at the root of the * tree. */ private static int hash(Object key) { return Integer.reverse(key.hashCode()); } /** Return the current bucket index from the hash. */ private static int bucket(int hash) { return hash >>> BITS_SHIFT; } /** Return a new hash with the next bucket number shifted off. */ private static int shift(int hash) { return hash << BITS; } /** Unshift the bucket number back onto a hash. */ private static int unshift(int hash, int bucket) { return (hash >>> BITS) | (bucket << BITS_SHIFT); } /** Compare two unsigned integers. */ private static int compareUnsigned(int left, int right) { // NOTE: JDK 7 does not have a built-in operation for this, other than casting to longs. // In JDK 8 it's just Integer.compareUnsigned(left, right). For now we emulate it // by shifting the sign bit away, with a fallback second compare only if needed. int diff = (left >>> 2) - (right >>> 2); return diff != 0 ? diff : (left & 3) - (right & 3); } /** * Returns a new version of this map with the given key at the root, and the root element moved to * some deeper node. If the key is not found, then value will be null. */ @SuppressWarnings("unchecked") private HamtPMap pivot(K key, int hash) { return pivot(key, hash, null, (V[]) new Object[1]); } /** * Internal recursive version of pivot. If parent is null then the result is used for the value in * the returned map. The value, if found, is stored in the 'result' array as a secondary return. */ private HamtPMap pivot(K key, int hash, @Nullable HamtPMap parent, V[] result) { int newMask = mask; HamtPMap[] newChildren = this.children; if (hash == this.hash && key.equals(this.key)) { // Found the key: swap out this key/value with the parent and return the result in the holder. result[0] = this.value; } else { // Otherwise, see if the value might be present in a child. int searchBucket = bucket(hash); int replacementBucket = bucket(this.hash); if (searchBucket == replacementBucket) { int bucketMask = 1 << searchBucket; if ((mask & bucketMask) != 0) { // Found a child, call pivot recursively. int index = index(bucketMask); HamtPMap child = newChildren[index]; HamtPMap newChild = child.pivot(key, shift(hash), this, result); newChildren = replaceChild(newChildren, index, newChild); } } else { int searchMask = 1 << searchBucket; if ((mask & searchMask) != 0) { int index = index(searchMask); HamtPMap child = newChildren[index]; HamtPMap newChild = child.minus(key, shift(hash), result); if (!newChild.isEmpty()) { newChildren = replaceChild(newChildren, index, newChild); } else { newChildren = deleteChild(newChildren, index); newMask &= ~searchMask; } } int replacementMask = 1 << replacementBucket; int index = Integer.bitCount(newMask & (replacementMask - 1)); if ((mask & replacementMask) != 0) { HamtPMap child = newChildren[index]; HamtPMap newChild = child.plus(this.key, shift(this.hash), this.value); newChildren = replaceChild(newChildren, index, newChild); } else { newChildren = insertChild( newChildren, index, new HamtPMap<>(this.key, shift(this.hash), this.value, 0, emptyChildren())); newMask |= replacementMask; } } } return parent != null ? new HamtPMap<>(parent.key, shift(parent.hash), parent.value, newMask, newChildren) : new HamtPMap<>(key, hash, result[0], newMask, newChildren); } /** Moves the root into the appropriate child. */ private HamtPMap vacateRoot() { int bucket = bucket(this.hash); int bucketMask = 1 << bucket; int index = index(bucketMask); if ((mask & bucketMask) != 0) { HamtPMap newChild = children[index].plus(this.key, shift(this.hash), this.value); return new HamtPMap<>(null, 0, null, mask, replaceChild(children, index, newChild)); } HamtPMap newChild = new HamtPMap<>(this.key, shift(this.hash), this.value, 0, emptyChildren()); return new HamtPMap<>(null, 0, null, mask | bucketMask, insertChild(children, index, newChild)); } /** Returns a copy of this node with a different array of children. */ private HamtPMap withChildren(int mask, HamtPMap[] children) { return mask == this.mask && children == this.children ? this : new HamtPMap<>(key, hash, value, mask, children); } /** * Returns a new map with the elements from children. One element is removed from one of the * children and promoted to a root node. If there are no children, returns null. */ private static @Nullable HamtPMap deleteRoot(int mask, HamtPMap[] children) { if (mask == 0) { return null; } HamtPMap child = children[0]; int hashBits = Integer.numberOfTrailingZeros(mask); int newHash = unshift(child.hash, hashBits); HamtPMap newChild = deleteRoot(child.mask, child.children); if (newChild == null) { int newMask = mask & ~Integer.lowestOneBit(mask); return new HamtPMap<>(child.key, newHash, child.value, newMask, deleteChild(children, 0)); } else { return new HamtPMap<>( child.key, newHash, child.value, mask, replaceChild(children, 0, newChild)); } } /** Returns a new array of children with an additional child inserted at the given index. */ private static HamtPMap[] insertChild( HamtPMap[] children, int index, HamtPMap child) { @SuppressWarnings("unchecked") // only used internally. HamtPMap[] newChildren = (HamtPMap[]) new HamtPMap[children.length + 1]; newChildren[index] = child; System.arraycopy(children, 0, newChildren, 0, index); System.arraycopy(children, index, newChildren, index + 1, children.length - index); return newChildren; } /** Returns a new array of children with the child at the given index replaced. */ private static HamtPMap[] replaceChild( HamtPMap[] children, int index, HamtPMap child) { HamtPMap[] newChildren = Arrays.copyOf(children, children.length); newChildren[index] = child; return newChildren; } /** Returns a new array of children with the child at the given index deleted. */ private static HamtPMap[] deleteChild( HamtPMap[] children, int index) { if (children.length == 1) { // Note: index should always be zero. return emptyChildren(); } @SuppressWarnings("unchecked") // only used internally. HamtPMap[] newChildren = (HamtPMap[]) new HamtPMap[children.length - 1]; System.arraycopy(children, 0, newChildren, 0, index); System.arraycopy(children, index + 1, newChildren, index, children.length - index - 1); return newChildren; } /** Iterates sequentially over a tree. */ private static class Iter implements Iterator { final Deque> queue = new ArrayDeque<>(); final Function, O> transformer; Iter(HamtPMap map, Function, O> transformer) { this.transformer = transformer; if (!map.isEmpty()) { queue.add(map); } } @Override public boolean hasNext() { return !queue.isEmpty(); } @Override public O next() { HamtPMap top = queue.removeFirst(); for (int i = top.children.length - 1; i >= 0; i--) { queue.add(top.children[i]); } return transformer.apply(top); } @Override public void remove() { throw new UnsupportedOperationException(); } } /** Throws an assertion error if the map invariant is violated. */ @VisibleForTesting HamtPMap assertCorrectStructure() { if (isEmpty()) { return this; } int hash = hash(key); for (int i = 0; i < children.length; i++) { int childHash = hash(children[i].key); if (compareUnsigned(childHash, hash) < 0) { throw new AssertionError("Invalid map has decreasing hash " + children[i].key + "(" + Integer.toHexString(childHash) + ") beneath " + key + "(" + Integer.toHexString(hash) + ": " + this); } children[i].assertCorrectStructure(); } return this; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy