blob: 39265a6875ac3c48d710cbca9db6ee9d729887e6 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.
package com.google.devtools.build.lib.collect.nestedset;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.collect.CompactHashSet;
import java.util.AbstractCollection;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
/**
* A list-like iterable that supports efficient nesting.
*
* @see NestedSetBuilder
*/
@SuppressWarnings("unchecked")
public final class NestedSet<E> implements Iterable<E> {
private final Order order;
private final Object children;
private byte[] memo;
private static final byte[] LEAF_MEMO = {};
private static final Object[] EMPTY_CHILDREN = {};
/**
* Construct an empty NestedSet. Should only be called by Order's class initializer.
*/
NestedSet(Order order) {
this.order = order;
this.children = EMPTY_CHILDREN;
this.memo = LEAF_MEMO;
}
NestedSet(Order order, Set<E> direct, Set<NestedSet<E>> transitive) {
this.order = order;
// The iteration order of these collections is the order in which we add the items.
Collection<E> directOrder = direct;
Collection<NestedSet<E>> transitiveOrder = transitive;
// True if we visit the direct members before the transitive members.
boolean preorder;
switch(order) {
case LINK_ORDER:
directOrder = ImmutableList.copyOf(direct).reverse();
transitiveOrder = ImmutableList.copyOf(transitive).reverse();
preorder = false;
break;
case STABLE_ORDER:
case COMPILE_ORDER:
preorder = false;
break;
case NAIVE_LINK_ORDER:
preorder = true;
break;
default:
throw new AssertionError(order);
}
// Remember children we extracted from one-element subsets.
// Otherwise we can end up with two of the same child, which is a
// problem for the fast path in toList().
Set<E> alreadyInserted = ImmutableSet.of();
// The candidate array of children.
Object[] children = new Object[direct.size() + transitive.size()];
int n = 0; // current position in children
boolean leaf = true; // until we find otherwise
for (int pass = 0; pass <= 1; ++pass) {
if ((pass == 0) == preorder && !direct.isEmpty()) {
for (E member : directOrder) {
if (member instanceof Object[]) {
throw new IllegalArgumentException("cannot store Object[] in NestedSet");
}
if (!alreadyInserted.contains(member)) {
children[n++] = member;
}
}
alreadyInserted = direct;
} else if ((pass == 1) == preorder && !transitive.isEmpty()) {
CompactHashSet<E> hoisted = CompactHashSet.create();
for (NestedSet<E> subset : transitiveOrder) {
Object c = subset.children;
if (c instanceof Object[]) {
Object[] a = (Object[]) c;
if (a.length < 2) {
throw new AssertionError(a.length);
}
children[n++] = a;
leaf = false;
} else {
if (!alreadyInserted.contains((E) c) && hoisted.add((E) c)) {
children[n++] = c;
}
}
}
alreadyInserted = hoisted;
}
}
// If we ended up wrapping exactly one item or one other set, dereference it.
if (n == 1) {
this.children = children[0];
} else if (n == 0) {
this.children = EMPTY_CHILDREN;
} else if (n < children.length) {
this.children = Arrays.copyOf(children, n);
} else {
this.children = children;
}
if (leaf) {
this.memo = LEAF_MEMO;
}
}
/**
* Returns the ordering of this nested set.
*/
public Order getOrder() {
return order;
}
/**
* Returns the internal item or array. For use by NestedSetVisitor.
*/
Object rawChildren() {
return children;
}
/**
* Returns true if the set is empty.
*/
public boolean isEmpty() {
return children == EMPTY_CHILDREN;
}
/**
* Returns true if the set has exactly one element.
*/
private boolean isSingleton() {
return !(children instanceof Object[]);
}
/**
* Returns a collection of all unique elements of this set (including subsets)
* in an implementation-specified order as a {@code Collection}.
*
* <p>If you do not need a Collection and an Iterable is enough, use the
* nested set itself as an Iterable.
*/
public Collection<E> toCollection() {
return toList();
}
/**
* Returns a collection of all unique elements of this set (including subsets)
* in an implementation-specified order as a {code List}.
*
* <p>Use {@link #toCollection} when possible for better efficiency.
*/
public List<E> toList() {
if (isSingleton()) {
return ImmutableList.of((E) children);
}
if (isEmpty()) {
return ImmutableList.of();
}
return order == Order.LINK_ORDER ? expand().reverse() : expand();
}
/**
* Returns a collection of all unique elements of this set (including subsets)
* in an implementation-specified order as a {@code Set}.
*
* <p>Use {@link #toCollection} when possible for better efficiency.
*/
public Set<E> toSet() {
return ImmutableSet.copyOf(toList());
}
/**
* Returns true if this set is equal to {@code other} based on the top-level
* elements and object identity (==) of direct subsets. As such, this function
* can fail to equate {@code this} with another {@code NestedSet} that holds
* the same elements. It will never fail to detect that two {@code NestedSet}s
* are different, however.
*
* @param other the {@code NestedSet} to compare against.
*/
public boolean shallowEquals(@Nullable NestedSet<? extends E> other) {
if (this == other) {
return true;
}
return other != null
&& order == other.order
&& (children.equals(other.children)
|| (!isSingleton() && !other.isSingleton()
&& Arrays.equals((Object[]) children, (Object[]) other.children)));
}
/**
* Returns a hash code that produces a notion of identity that is consistent with
* {@link #shallowEquals}. In other words, if two {@code NestedSet}s are equal according
* to {@code #shallowEquals}, then they return the same {@code shallowHashCode}.
*
* <p>The main reason for having these separate functions instead of reusing
* the standard equals/hashCode is to minimize accidental use, since they are
* different from both standard Java objects and collection-like objects.
*/
public int shallowHashCode() {
if (isSingleton()) {
return Objects.hash(order, children);
} else {
return Objects.hash(order, Arrays.hashCode((Object[]) children));
}
}
@Override
public String toString() {
return isSingleton() ? "{" + children + "}" : childrenToString(children);
}
// TODO: this leaves LINK_ORDER backwards
private static String childrenToString(Object children) {
if (children instanceof Object[]) {
return "{" + Joiner.on(", ").join(Iterables.transform(
Arrays.asList((Object[]) children), Stringer.INSTANCE)) + "}";
} else {
return children.toString();
}
}
private static enum Stringer implements Function<Object, String> {
INSTANCE;
@Override public String apply(Object o) {
return childrenToString(o);
}
};
@Override
public Iterator<E> iterator() {
// TODO: would it help to have a proper lazy iterator? seems like it might reduce garbage.
return toCollection().iterator();
}
/**
* Implementation of {@link #toList}. Uses one of three strategies based on the value of
* {@code this.memo}: wrap our direct items in a list, call {@link #lockedExpand} to perform
* the initial {@link #walk}, or call {@link #replay} if we have a nontrivial memo.
*/
private ImmutableList<E> expand() {
// This value is only set in the constructor, so safe to test here with no lock.
if (memo == LEAF_MEMO) {
return ImmutableList.<E>copyOf(new ArraySharingCollection<E>((Object[]) children));
}
CompactHashSet<E> members = lockedExpand();
if (members != null) {
return ImmutableList.copyOf(members);
}
Object[] children = (Object[]) this.children;
// TODO: We could record the exact size (inside memo, or by making order an int with two bits
// for Order.ordinal()) and avoid an array copy here. It's not directly visible in profiles but
// it would reduce garbage generated.
ImmutableList.Builder<E> output = ImmutableList.builder();
replay(output, children, memo, 0);
return output.build();
}
// Hack to share our internal array with ImmutableList/ImmutableSet, or avoid
// a copy in cases where we can preallocate an array of the correct size.
private static final class ArraySharingCollection<E> extends AbstractCollection<E> {
private final Object[] array;
ArraySharingCollection(Object[] array) {
this.array = array;
}
@Override public Object[] toArray() {
return array;
}
@Override public int size() {
return array.length;
}
@Override public Iterator<E> iterator() {
throw new UnsupportedOperationException();
}
}
/**
* If this is the first call for this object, fills {@code this.memo} and returns a set from
* {@link #walk}. Otherwise returns null; the caller should use {@link #replay} instead.
*/
private synchronized CompactHashSet<E> lockedExpand() {
if (memo != null) {
return null;
}
Object[] children = (Object[]) this.children;
CompactHashSet<E> members = CompactHashSet.createWithExpectedSize(128);
CompactHashSet<Object> sets = CompactHashSet.createWithExpectedSize(128);
sets.add(children);
memo = new byte[Math.min((children.length + 7) / 8, 8)];
int pos = walk(sets, members, children, 0);
int bytes = (pos + 7) / 8;
if (bytes <= memo.length - 16) {
memo = Arrays.copyOf(memo, bytes);
}
return members;
}
/**
* Perform a depth-first traversal of {@code children}, tracking visited
* arrays in {@code sets} and visited leaves in {@code members}. We also
* record which edges were taken in {@code this.memo} starting at {@code pos}.
*
* Returns the final value of {@code pos}.
*/
private int walk(CompactHashSet<Object> sets, CompactHashSet<E> members,
Object[] children, int pos) {
int n = children.length;
for (int i = 0; i < n; ++i) {
if ((pos>>3) >= memo.length) {
memo = Arrays.copyOf(memo, memo.length * 2);
}
Object c = children[i];
if (c instanceof Object[]) {
if (sets.add(c)) {
int prepos = pos;
int presize = members.size();
pos = walk(sets, members, (Object[]) c, pos + 1);
if (presize < members.size()) {
memo[prepos>>3] |= 1<<(prepos&7);
} else {
// We didn't find any new nodes, so don't mark this branch as taken.
// Rewind pos. The rest of the array is still zeros because no one
// deeper in the traversal set any bits.
pos = prepos + 1;
}
} else {
++pos;
}
} else {
if (members.add((E) c)) {
memo[pos>>3] |= 1<<(pos&7);
}
++pos;
}
}
return pos;
}
/**
* Repeat a previous traversal of {@code children} performed by {@link #walk}
* and recorded in {@code memo}, appending leaves to {@code output}.
*/
private static <E> int replay(ImmutableList.Builder<E> output, Object[] children,
byte[] memo, int pos) {
int n = children.length;
for (int i = 0; i < n; ++i) {
Object c = children[i];
if ((memo[pos>>3] & (1<<(pos&7))) != 0) {
if (c instanceof Object[]) {
pos = replay(output, (Object[]) c, memo, pos + 1);
} else {
output.add((E) c);
++pos;
}
} else {
++pos;
}
}
return pos;
}
}