blob: 83fc14bd16e9741bd5d2a0078908964f12e80e42 [file]
// Copyright 2026 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 net.starlark.java.eval;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.BiConsumer;
/**
* A deeply immutable {@link Dict} with a custom memory-efficient implementation.
*
* <p>Construct an instance by calling {@link #copyOf(Map)}. Iteration order of the given map is
* preserved.
*
* <p>Size cutoffs for the various specialized implementations were chosen using the frequency
* distribution of dict instances from an example large build in b/507408768#comment3. Compared to
* {@link ImmutableMap}, additional memory savings come from:
*
* <ol>
* <li>All sizes: no caching of collection views in {@link #keySet}, {@link #values}, and {@link
* #entrySet}.
* <li>Size 2: dedicated {@link DoubletonImmutableDict}.
* <li>Sizes 3+: {@link ArrayImmutableDict} shares backing key and value arrays with {@link
* RegularImmutableStarlarkList}.
* <li>Sizes 3-8: {@link LinearImmutableDict} uses linear search with no hash table.
* <li>Sizes 9+: {@link HashImmutableDict} uses open hashing instead of entry wrappers.
* </ol>
*
* <p>{@link #equals} and {@link #hashCode} are order-independent and compatible with arbitrary
* {@link Map} instances. All {@link #equals} implementations catch {@link ClassCastException} and
* {@link NullPointerException} when calling {@link Map#get} on the given map. This is required by
* the {@link Map#equals} contract to safely handle comparisons with arbitrary or type-restricted
* maps where our keys might be incompatible.
*/
abstract sealed class CompactImmutableDict<K, V> extends Dict<K, V> {
@SuppressWarnings("unchecked")
public static <K, V> CompactImmutableDict<K, V> empty() {
return (CompactImmutableDict<K, V>) EmptyImmutableDict.INSTANCE;
}
/**
* Creates an immutable, compact version of the given map.
*
* <p>Callers are responsible for ensuring that all keys are {@linkplain Starlark#checkHashable
* hashable} and all values are {@linkplain Starlark#checkValid valid} starlark objects, which
* implies that they are non-null.
*/
@SuppressWarnings("unchecked")
static <K, V> CompactImmutableDict<K, V> copyOf(Map<? extends K, ? extends V> m) {
if (m instanceof CompactImmutableDict<?, ?> dict) {
return (CompactImmutableDict<K, V>) dict;
}
int size = m.size();
return switch (size) {
case 0 -> empty();
case 1 -> {
var e = m.entrySet().iterator().next();
yield new SingletonImmutableDict<>(e.getKey(), e.getValue());
}
case 2 -> {
var it = m.entrySet().iterator();
var e1 = it.next();
var e2 = it.next();
yield new DoubletonImmutableDict<>(e1.getKey(), e1.getValue(), e2.getKey(), e2.getValue());
}
default -> {
K[] ks = (K[]) new Object[size];
V[] vs = (V[]) new Object[size];
int i = 0;
for (var e : m.entrySet()) {
ks[i] = e.getKey();
vs[i] = e.getValue();
i++;
}
yield size <= 8 ? new LinearImmutableDict<>(ks, vs) : new HashImmutableDict<>(ks, vs);
}
};
}
@Override
public final Mutability mutability() {
return Mutability.IMMUTABLE;
}
@Override
public final boolean updateIteratorCount(int delta) {
return false;
}
@Override
public final void putEntry(K key, V value) throws EvalException {
throw immutable();
}
@Override
public final <K2 extends K, V2 extends V> void putEntries(Map<K2, V2> map) throws EvalException {
throw immutable();
}
@Override
public final void clearEntries() throws EvalException {
throw immutable();
}
@Override
public final Object pop(Object key, Object defaultValue, StarlarkThread thread)
throws EvalException {
throw immutable();
}
@Override
public final Tuple popitem() throws EvalException {
if (isEmpty()) {
throw Starlark.errorf("popitem: empty dictionary");
}
throw immutable();
}
@Override
public final V setdefault(K key, V defaultValue) throws EvalException {
throw immutable();
}
private EvalException immutable() throws EvalException {
Starlark.checkMutable(this);
throw new IllegalStateException();
}
/** Specialized singleton implementation for an empty dict. */
private static final class EmptyImmutableDict<K, V> extends CompactImmutableDict<K, V> {
static final EmptyImmutableDict<?, ?> INSTANCE = new EmptyImmutableDict<>();
@Override
public StarlarkList<?> values0(StarlarkThread thread) {
return StarlarkList.newList(thread.mutability());
}
@Override
public StarlarkList<?> items(StarlarkThread thread) {
return StarlarkList.newList(thread.mutability());
}
@Override
public StarlarkList<?> keys(StarlarkThread thread) {
return StarlarkList.newList(thread.mutability());
}
@Override
public Iterator<K> iterator() {
return Collections.emptyIterator();
}
@Override
public int size() {
return 0;
}
@Override
public boolean containsKey(Object key) {
return false;
}
@Override
public boolean containsValue(Object value) {
return false;
}
@Override
public V get(Object key) {
return null;
}
@Override
public ImmutableSet<K> keySet() {
return ImmutableSet.of();
}
@Override
public ImmutableList<V> values() {
return ImmutableList.of();
}
@Override
public ImmutableSet<Entry<K, V>> entrySet() {
return ImmutableSet.of();
}
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
checkNotNull(action);
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
return o instanceof Map<?, ?> m && m.isEmpty();
}
}
/** Specialized implementation for a dict of size 1. */
private static final class SingletonImmutableDict<K, V> extends CompactImmutableDict<K, V> {
private final K k;
private final V v;
SingletonImmutableDict(K k, V v) {
this.k = k;
this.v = v;
}
@Override
public StarlarkList<?> values0(StarlarkThread thread) {
return StarlarkList.wrap(thread.mutability(), new Object[] {v});
}
@Override
public StarlarkList<?> items(StarlarkThread thread) {
return StarlarkList.wrap(thread.mutability(), new Object[] {Tuple.pair(k, v)});
}
@Override
public StarlarkList<?> keys(StarlarkThread thread) {
return StarlarkList.wrap(thread.mutability(), new Object[] {k});
}
@Override
public Iterator<K> iterator() {
return Iterators.singletonIterator(k);
}
@Override
public int size() {
return 1;
}
@Override
public boolean containsKey(Object key) {
return k.equals(key);
}
@Override
public boolean containsValue(Object value) {
return v.equals(value);
}
@Override
public V get(Object key) {
return k.equals(key) ? v : null;
}
@Override
public ImmutableSet<K> keySet() {
return ImmutableSet.of(k);
}
@Override
public ImmutableList<V> values() {
return ImmutableList.of(v);
}
@Override
public ImmutableSet<Entry<K, V>> entrySet() {
return ImmutableSet.of(Maps.immutableEntry(k, v));
}
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
action.accept(k, v);
}
@Override
public int hashCode() {
return k.hashCode() ^ v.hashCode();
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Map<?, ?> m)) {
return false;
}
if (m.size() != 1) {
return false;
}
try {
return v.equals(m.get(k));
} catch (ClassCastException | NullPointerException unused) {
return false;
}
}
}
/** Specialized implementation for a dict of size 2. */
private static final class DoubletonImmutableDict<K, V> extends CompactImmutableDict<K, V> {
private final K k1;
private final V v1;
private final K k2;
private final V v2;
DoubletonImmutableDict(K k1, V v1, K k2, V v2) {
this.k1 = k1;
this.v1 = v1;
this.k2 = k2;
this.v2 = v2;
}
@Override
public StarlarkList<?> values0(StarlarkThread thread) {
return StarlarkList.wrap(thread.mutability(), new Object[] {v1, v2});
}
@Override
public StarlarkList<?> items(StarlarkThread thread) {
return StarlarkList.wrap(
thread.mutability(), new Object[] {Tuple.pair(k1, v1), Tuple.pair(k2, v2)});
}
@Override
public StarlarkList<?> keys(StarlarkThread thread) {
return StarlarkList.wrap(thread.mutability(), new Object[] {k1, k2});
}
@Override
public Iterator<K> iterator() {
return Iterators.forArray(k1, k2);
}
@Override
public int size() {
return 2;
}
@Override
public boolean containsKey(Object key) {
return k1.equals(key) || k2.equals(key);
}
@Override
public boolean containsValue(Object value) {
return v1.equals(value) || v2.equals(value);
}
@Override
public V get(Object key) {
if (k1.equals(key)) {
return v1;
}
if (k2.equals(key)) {
return v2;
}
return null;
}
@Override
public ImmutableSet<K> keySet() {
return ImmutableSet.of(k1, k2);
}
@Override
public ImmutableList<V> values() {
return ImmutableList.of(v1, v2);
}
@Override
public ImmutableSet<Entry<K, V>> entrySet() {
return ImmutableSet.of(Maps.immutableEntry(k1, v1), Maps.immutableEntry(k2, v2));
}
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
action.accept(k1, v1);
action.accept(k2, v2);
}
@Override
public int hashCode() {
return (k1.hashCode() ^ v1.hashCode()) + (k2.hashCode() ^ v2.hashCode());
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Map<?, ?> m)) {
return false;
}
if (m.size() != 2) {
return false;
}
try {
return v1.equals(m.get(k1)) && v2.equals(m.get(k2));
} catch (ClassCastException | NullPointerException unused) {
return false;
}
}
}
/** Partial implementation based on parallel key-value arrays. */
private abstract static sealed class ArrayImmutableDict<K, V> extends CompactImmutableDict<K, V> {
final K[] ks;
final V[] vs;
ArrayImmutableDict(K[] ks, V[] vs) {
this.ks = ks;
this.vs = vs;
}
@Override
public final StarlarkList<?> values0(StarlarkThread thread) {
return StarlarkList.wrap(thread.mutability(), vs.clone());
}
@Override
public final StarlarkList<?> items(StarlarkThread thread) {
Object[] items = new Object[ks.length];
for (int i = 0; i < ks.length; i++) {
items[i] = Tuple.pair(ks[i], vs[i]);
}
return StarlarkList.wrap(thread.mutability(), items);
}
@Override
public final StarlarkList<?> keys(StarlarkThread thread) {
return StarlarkList.wrap(thread.mutability(), ks.clone());
}
@Override
public final Iterator<K> iterator() {
return Iterators.forArray(ks);
}
@Override
public final int size() {
return ks.length;
}
@Override
public final boolean containsValue(Object value) {
if (value == null) {
return false;
}
for (V v : vs) {
if (v.equals(value)) {
return true;
}
}
return false;
}
@Override
public final Set<K> keySet() {
return new AbstractSet<>() {
@Override
public Iterator<K> iterator() {
return Iterators.forArray(ks);
}
@Override
public int size() {
return ks.length;
}
@Override
public boolean contains(Object o) {
return containsKey(o);
}
};
}
@Override
public final StarlarkList<V> values() {
return new RegularImmutableStarlarkList<>(vs);
}
@Override
public final Set<Entry<K, V>> entrySet() {
return new AbstractSet<>() {
@Override
public Iterator<Entry<K, V>> iterator() {
return new Iterator<>() {
private int i = 0;
@Override
public boolean hasNext() {
return i < ks.length;
}
@Override
public Entry<K, V> next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
var e = Maps.immutableEntry(ks[i], vs[i]);
i++;
return e;
}
};
}
@Override
public int size() {
return ks.length;
}
@Override
public boolean contains(Object o) {
if (!(o instanceof Map.Entry<?, ?> e) || e.getValue() == null) {
return false;
}
return e.getValue().equals(get(e.getKey()));
}
};
}
@Override
public final void forEach(BiConsumer<? super K, ? super V> action) {
for (int i = 0; i < ks.length; i++) {
action.accept(ks[i], vs[i]);
}
}
@Override
public final int hashCode() {
int h = 0;
for (int i = 0; i < ks.length; i++) {
h += (ks[i].hashCode() ^ vs[i].hashCode());
}
return h;
}
@Override
public final boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Map<?, ?> m)) {
return false;
}
if (m.size() != ks.length) {
return false;
}
try {
for (int i = 0; i < ks.length; i++) {
if (!vs[i].equals(m.get(ks[i]))) {
return false;
}
}
return true;
} catch (ClassCastException | NullPointerException unused) {
return false;
}
}
}
/**
* Implementation for small dicts where linear search is expected to perform just as well as a
* hash table.
*/
private static final class LinearImmutableDict<K, V> extends ArrayImmutableDict<K, V> {
LinearImmutableDict(K[] ks, V[] vs) {
super(ks, vs);
}
@Override
public boolean containsKey(Object key) {
if (key == null) {
return false;
}
for (K k : ks) {
if (key.equals(k)) {
return true;
}
}
return false;
}
@Override
public V get(Object key) {
if (key == null) {
return null;
}
for (int i = 0; i < ks.length; i++) {
if (key.equals(ks[i])) {
return vs[i];
}
}
return null;
}
}
/** Open hash table implementation. */
private static final class HashImmutableDict<K, V> extends ArrayImmutableDict<K, V> {
// Values are the index of the corresponding element in ks and vs, or -1 for empty.
private final int[] table;
HashImmutableDict(K[] ks, V[] vs) {
super(ks, vs);
int n = ks.length;
int tableSize = n * 2; // 0.5 load factor.
int[] table = new int[tableSize];
Arrays.fill(table, -1);
for (int i = 0; i < n; i++) {
int idx = getTableIndex(ks[i], tableSize);
while (table[idx] != -1) {
if (++idx == tableSize) {
idx = 0;
}
}
table[idx] = i;
}
this.table = table;
}
private static int getTableIndex(Object k, int tableSize) {
int hash = k.hashCode();
hash = hash ^ (hash >>> 16);
return (hash & 0x7fffffff) % tableSize;
}
private int getTableIndex(Object k) {
return getTableIndex(k, table.length);
}
@Override
public boolean containsKey(Object key) {
return get(key) != null;
}
@Override
public V get(Object key) {
if (key == null) {
return null;
}
int tableIdx = getTableIndex(key);
int kvIdx;
while ((kvIdx = table[tableIdx]) != -1) {
if (key.equals(ks[kvIdx])) {
return vs[kvIdx];
}
if (++tableIdx == table.length) {
tableIdx = 0;
}
}
return null;
}
}
}