8356080: Address post-integration comments for Stable Values

Reviewed-by: liach
This commit is contained in:
Per Minborg 2025-05-13 13:40:48 +00:00
parent fa419489d3
commit 066477de80
14 changed files with 388 additions and 131 deletions

View File

@ -384,8 +384,8 @@ import java.util.function.Supplier;
* <p>
* The method {@link #orElseSet(Supplier)} guarantees that the provided
* {@linkplain Supplier} is invoked successfully at most once, even under race.
* Invocations of {@link #setOrThrow(Object)} form a total order of zero or more
* exceptional invocations followed by zero (if the contents were already set) or one
* Invocations of {@link #orElseSet(Supplier)} form a total order of zero or
* more exceptional invocations followed by zero (if the contents were already set) or one
* successful invocation. Since stable functions and stable collections are built on top
* of the same principles as {@linkplain StableValue#orElseSet(Supplier) orElseSet()} they
* too are thread safe and guarantee at-most-once-per-input invocation.
@ -444,8 +444,6 @@ import java.util.function.Supplier;
public sealed interface StableValue<T>
permits StableValueImpl {
// Principal methods
/**
* Tries to set the contents of this StableValue to the provided {@code contents}.
* The contents of this StableValue can only be set once, implying this method only
@ -509,8 +507,6 @@ public sealed interface StableValue<T>
*/
T orElseSet(Supplier<? extends T> supplier);
// Convenience methods
/**
* Sets the contents of this StableValue to the provided {@code contents}, or, if
* already set, throws {@code IllegalStateException}.
@ -519,6 +515,9 @@ public sealed interface StableValue<T>
*
* @param contents to set
* @throws IllegalStateException if the contents was already set
* @throws IllegalStateException if a supplier invoked by {@link #orElseSet(Supplier)}
* recursively attempts to set this stable value by calling this method
* directly or indirectly.
*/
void setOrThrow(T contents);
@ -573,8 +572,8 @@ public sealed interface StableValue<T>
* at most once even in a multi-threaded environment. Competing threads invoking the
* returned supplier's {@linkplain Supplier#get() get()} method when a value is
* already under computation will block until a value is computed or an exception is
* thrown by the computing thread. The computing threads will then observe the newly
* computed value (if any) and will then never execute.
* thrown by the computing thread. The competing threads will then observe the newly
* computed value (if any) and will then never execute the {@code underlying} supplier.
* <p>
* If the provided {@code underlying} supplier throws an exception, it is rethrown
* to the initial caller and no contents is recorded.
@ -614,9 +613,9 @@ public sealed interface StableValue<T>
* function for the same input, an {@linkplain IllegalStateException} will
* be thrown.
*
* @param size the size of the allowed inputs in the continuous
* interval {@code [0, size)}
* @param underlying IntFunction used to compute cached values
* @param size the upper bound of the range {@code [0, size)} indicating
* the allowed inputs
* @param underlying {@code IntFunction} used to compute cached values
* @param <R> the type of results delivered by the returned IntFunction
* @throws IllegalArgumentException if the provided {@code size} is negative.
*/
@ -684,7 +683,7 @@ public sealed interface StableValue<T>
* If invoking the provided {@code mapper} function throws an exception, it
* is rethrown to the initial caller and no value for the element is recorded.
* <p>
* Any direct {@link List#subList(int, int) subList} or {@link List#reversed()} views
* Any {@link List#subList(int, int) subList} or {@link List#reversed()} views
* of the returned list are also stable.
* <p>
* The returned list and its {@link List#subList(int, int) subList} or
@ -727,8 +726,8 @@ public sealed interface StableValue<T>
* is rethrown to the initial caller and no value associated with the provided key
* is recorded.
* <p>
* Any direct {@link Map#values()} or {@link Map#entrySet()} views
* of the returned map are also stable.
* Any {@link Map#values()} or {@link Map#entrySet()} views of the returned map are
* also stable.
* <p>
* The returned map is unmodifiable and does not implement the
* {@linkplain Collection##optional-operations optional operations} in the

View File

@ -47,7 +47,6 @@ import jdk.internal.lang.stable.StableUtil;
import jdk.internal.lang.stable.StableValueImpl;
import jdk.internal.misc.CDS;
import jdk.internal.util.ArraysSupport;
import jdk.internal.util.NullableKeyValueHolder;
import jdk.internal.vm.annotation.ForceInline;
import jdk.internal.vm.annotation.Stable;
@ -137,9 +136,11 @@ class ImmutableCollections {
return ImmutableCollections.listFromTrustedArrayNullsAllowed(array);
}
public <E> List<E> stableList(int size, IntFunction<? extends E> mapper) {
return ImmutableCollections.stableList(size, mapper);
// A stable list is not Serializable, so we cannot return `List.of()` if `size == 0`
return new StableList<>(size, mapper);
}
public <K, V> Map<K, V> stableMap(Set<K> keys, Function<? super K, ? extends V> mapper) {
// A stable map is not Serializable, so we cannot return `Map.of()` if `keys.isEmpty()`
return new StableMap<>(keys, mapper);
}
});
@ -264,11 +265,6 @@ class ImmutableCollections {
}
}
static <E> List<E> stableList(int size, IntFunction<? extends E> mapper) {
// A lazy list is not Serializable so, we cannot return `List.of()` if size == 0
return new StableList<>(size, mapper);
}
// ---------- List Implementations ----------
@jdk.internal.ValueBased
@ -454,17 +450,17 @@ class ImmutableCollections {
}
}
static final class SubList<E> extends AbstractImmutableList<E>
static sealed class SubList<E> extends AbstractImmutableList<E>
implements RandomAccess {
@Stable
private final AbstractImmutableList<E> root;
final AbstractImmutableList<E> root;
@Stable
private final int offset;
final int offset;
@Stable
private final int size;
final int size;
private SubList(AbstractImmutableList<E> root, int offset, int size) {
assert root instanceof List12 || root instanceof ListN || root instanceof StableList;
@ -517,9 +513,8 @@ class ImmutableCollections {
}
}
private boolean allowNulls() {
return root instanceof ListN<?> listN && listN.allowNulls
|| root instanceof StableList<E>;
boolean allowNulls() {
return root instanceof ListN<?> listN && listN.allowNulls;
}
@Override
@ -572,14 +567,6 @@ class ImmutableCollections {
return array;
}
@Override
public String toString() {
if (root instanceof StableList<E> stableList) {
return StableUtil.renderElements(root, "StableList", stableList.delegates, offset, size);
} else {
return super.toString();
}
}
}
@jdk.internal.ValueBased
@ -797,8 +784,15 @@ class ImmutableCollections {
}
}
@FunctionalInterface
interface HasStableDelegates<E> {
StableValueImpl<E>[] delegates();
}
@jdk.internal.ValueBased
static final class StableList<E> extends AbstractImmutableList<E> {
static final class StableList<E>
extends AbstractImmutableList<E>
implements HasStableDelegates<E> {
@Stable
private final IntFunction<? extends E> mapper;
@ -879,11 +873,69 @@ class ImmutableCollections {
}
@Override
public String toString() {
return StableUtil.renderElements(this, "StableList", delegates);
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size());
return StableSubList.fromStableList(this, fromIndex, toIndex);
}
private static final class StableReverseOrderListView<E> extends ReverseOrderListView.Rand<E> {
@Override
public String toString() {
return StableUtil.renderElements(this, "StableCollection", delegates);
}
@Override
public StableValueImpl<E>[] delegates() {
return delegates;
}
private static final class StableSubList<E> extends SubList<E>
implements HasStableDelegates<E> {
private StableSubList(AbstractImmutableList<E> root, int offset, int size) {
super(root, offset, size);
}
@Override
public List<E> reversed() {
return new StableReverseOrderListView<>(this);
}
@Override
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size());
return StableSubList.fromStableSubList(this, fromIndex, toIndex);
}
@Override
public String toString() {
return StableUtil.renderElements(this, "StableCollection", delegates());
}
@Override
boolean allowNulls() {
return true;
}
@Override
public StableValueImpl<E>[] delegates() {
@SuppressWarnings("unchecked")
final var rootDelegates = ((HasStableDelegates<E>) root).delegates();
return Arrays.copyOfRange(rootDelegates, offset, offset + size);
}
static <E> SubList<E> fromStableList(StableList<E> list, int fromIndex, int toIndex) {
return new StableSubList<>(list, fromIndex, toIndex - fromIndex);
}
static <E> SubList<E> fromStableSubList(StableSubList<E> parent, int fromIndex, int toIndex) {
return new StableSubList<>(parent.root, parent.offset + fromIndex, toIndex - fromIndex);
}
}
private static final class StableReverseOrderListView<E>
extends ReverseOrderListView.Rand<E>
implements HasStableDelegates<E> {
private StableReverseOrderListView(List<E> base) {
super(base, false);
@ -892,17 +944,23 @@ class ImmutableCollections {
// This method does not evaluate the elements
@Override
public String toString() {
final StableValueImpl<E>[] delegates = ((StableList<E>)base).delegates;
final StableValueImpl<E>[] reversed = ArraysSupport.reverse(
Arrays.copyOf(delegates, delegates.length));
return StableUtil.renderElements(base, "Collection", reversed);
return StableUtil.renderElements(this, "StableCollection", delegates());
}
@Override
public List<E> reversed() {
return base;
public List<E> subList(int fromIndex, int toIndex) {
final int size = base.size();
subListRangeCheck(fromIndex, toIndex, size);
return new StableReverseOrderListView<>(base.subList(size - toIndex, size - fromIndex));
}
@Override
public StableValueImpl<E>[] delegates() {
@SuppressWarnings("unchecked")
final var baseDelegates = ((HasStableDelegates<E>) base).delegates();
return ArraysSupport.reverse(
Arrays.copyOf(baseDelegates, baseDelegates.length));
}
}
}
@ -1560,7 +1618,7 @@ class ImmutableCollections {
@Override public boolean containsKey(Object o) { return delegate.containsKey(o); }
@Override public int size() { return delegate.size(); }
@Override public Set<Map.Entry<K, V>> entrySet() { return new StableMapEntrySet(); }
@Override public Set<Map.Entry<K, V>> entrySet() { return StableMapEntrySet.of(this); }
@ForceInline
@Override
@ -1582,32 +1640,49 @@ class ImmutableCollections {
}
@jdk.internal.ValueBased
final class StableMapEntrySet extends AbstractImmutableSet<Map.Entry<K, V>> {
static final class StableMapEntrySet<K, V> extends AbstractImmutableSet<Map.Entry<K, V>> {
// Use a separate field for the outer class in order to facilitate
// a @Stable annotation.
@Stable
private final StableMap<K, V> outer;
@Stable
private final Set<Map.Entry<K, StableValueImpl<V>>> delegateEntrySet;
StableMapEntrySet() {
this.delegateEntrySet = delegate.entrySet();
private StableMapEntrySet(StableMap<K, V> outer) {
this.outer = outer;
this.delegateEntrySet = outer.delegate.entrySet();
}
@Override public Iterator<Map.Entry<K, V>> iterator() { return new LazyMapIterator(); }
@Override public Iterator<Map.Entry<K, V>> iterator() { return LazyMapIterator.of(this); }
@Override public int size() { return delegateEntrySet.size(); }
@Override public int hashCode() { return StableMap.this.hashCode(); }
@Override public int hashCode() { return outer.hashCode(); }
@Override
public String toString() {
return StableUtil.renderMappings(this, "StableSet", delegateEntrySet, false);
return StableUtil.renderMappings(this, "StableCollection", delegateEntrySet, false);
}
// For @ValueBased
private static <K, V> StableMapEntrySet<K, V> of(StableMap<K, V> outer) {
return new StableMapEntrySet<>(outer);
}
@jdk.internal.ValueBased
final class LazyMapIterator implements Iterator<Map.Entry<K, V>> {
static final class LazyMapIterator<K, V> implements Iterator<Map.Entry<K, V>> {
// Use a separate field for the outer class in order to facilitate
// a @Stable annotation.
@Stable
private final StableMapEntrySet<K, V> outer;
@Stable
private final Iterator<Map.Entry<K, StableValueImpl<V>>> delegateIterator;
LazyMapIterator() {
this.delegateIterator = delegateEntrySet.iterator();
private LazyMapIterator(StableMapEntrySet<K, V> outer) {
this.outer = outer;
this.delegateIterator = outer.delegateEntrySet.iterator();
}
@Override public boolean hasNext() { return delegateIterator.hasNext(); }
@ -1616,8 +1691,8 @@ class ImmutableCollections {
public Entry<K, V> next() {
final Map.Entry<K, StableValueImpl<V>> inner = delegateIterator.next();
final K k = inner.getKey();
return new NullableKeyValueHolder<>(k, inner.getValue().orElseSet(new Supplier<V>() {
@Override public V get() { return mapper.apply(k); }}));
return new StableEntry<>(k, inner.getValue(), new Supplier<V>() {
@Override public V get() { return outer.outer.mapper.apply(k); }});
}
@Override
@ -1627,25 +1702,60 @@ class ImmutableCollections {
@Override
public void accept(Entry<K, StableValueImpl<V>> inner) {
final K k = inner.getKey();
action.accept(new NullableKeyValueHolder<>(k, inner.getValue().orElseSet(new Supplier<V>() {
@Override public V get() { return mapper.apply(k); }})));
action.accept(new StableEntry<>(k, inner.getValue(), new Supplier<V>() {
@Override public V get() { return outer.outer.mapper.apply(k); }}));
}
};
delegateIterator.forEachRemaining(innerAction);
}
// For @ValueBased
private static <K, V> LazyMapIterator<K, V> of(StableMapEntrySet<K, V> outer) {
return new LazyMapIterator<>(outer);
}
}
}
private record StableEntry<K, V>(K getKey, // trick
StableValueImpl<V> stableValue,
Supplier<? extends V> supplier) implements Map.Entry<K, V> {
@Override public V setValue(V value) { throw uoe(); }
@Override public V getValue() { return stableValue.orElseSet(supplier); }
@Override public int hashCode() { return hash(getKey()) ^ hash(getValue()); }
@Override public String toString() { return getKey() + "=" + stableValue.toString(); }
@Override public boolean equals(Object o) {
return o instanceof Map.Entry<?, ?> e
&& Objects.equals(getKey(), e.getKey())
// Invoke `getValue()` as late as possible to avoid evaluation
&& Objects.equals(getValue(), e.getValue());
}
private int hash(Object obj) { return (obj == null) ? 0 : obj.hashCode(); }
}
@Override
public Collection<V> values() {
return new StableMapValues();
return StableMapValues.of(this);
}
final class StableMapValues extends AbstractImmutableCollection<V> {
@Override public Iterator<V> iterator() { return new ValueIterator(); }
@Override public int size() { return StableMap.this.size(); }
@Override public boolean isEmpty() { return StableMap.this.isEmpty();}
@Override public boolean contains(Object v) { return StableMap.this.containsValue(v); }
@jdk.internal.ValueBased
static final class StableMapValues<V> extends AbstractImmutableCollection<V> {
// Use a separate field for the outer class in order to facilitate
// a @Stable annotation.
@Stable
private final StableMap<?, V> outer;
private StableMapValues(StableMap<?, V> outer) {
this.outer = outer;
}
@Override public Iterator<V> iterator() { return outer.new ValueIterator(); }
@Override public int size() { return outer.size(); }
@Override public boolean isEmpty() { return outer.isEmpty();}
@Override public boolean contains(Object v) { return outer.containsValue(v); }
private static final IntFunction<StableValueImpl<?>[]> GENERATOR = new IntFunction<StableValueImpl<?>[]>() {
@Override
@ -1656,9 +1766,15 @@ class ImmutableCollections {
@Override
public String toString() {
final StableValueImpl<?>[] values = delegate.values().toArray(GENERATOR);
return StableUtil.renderElements(StableMap.this, "StableMap", values);
final StableValueImpl<?>[] values = outer.delegate.values().toArray(GENERATOR);
return StableUtil.renderElements(this, "StableCollection", values);
}
// For @ValueBased
private static <V> StableMapValues<V> of(StableMap<?, V> outer) {
return new StableMapValues<>(outer);
}
}
@Override

View File

@ -33,14 +33,17 @@ import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jdk.internal.util.ArraysSupport;
import jdk.internal.vm.annotation.Stable;
/**
* Provides a reverse-ordered view of a List. Not serializable.
*/
class ReverseOrderListView<E> implements List<E> {
@Stable
final List<E> base;
final boolean modifiable;
@Stable
final Boolean modifiable;
public static <T> List<T> of(List<T> list, boolean modifiable) {
if (list instanceof RandomAccess) {

View File

@ -46,7 +46,10 @@ import java.util.function.Supplier;
* @implNote This implementation can be used early in the boot sequence as it does not
* rely on reflection, MethodHandles, Streams etc.
*
* @param enumType the class type of the Enum
* @param firstOrdinal the lowest ordinal used
* @param member an int predicate that can be used to test if an enum is a member
* of the valid inputs (as there might be "holes")
* @param delegates a delegate array of inputs to StableValue mappings
* @param original the original Function
* @param <E> the type of the input to the function
@ -64,10 +67,8 @@ public record StableEnumFunction<E extends Enum<E>, R>(Class<E> enumType,
throw new IllegalArgumentException("Input not allowed: " + value);
}
final int index = value.ordinal() - firstOrdinal;
final StableValueImpl<R> delegate;
// Since we did the member.test above, we know the index is in bounds
delegate = delegates[index];
return delegate.orElseSet(new Supplier<R>() {
return delegates[index].orElseSet(new Supplier<R>() {
@Override public R get() { return original.apply(value); }});
}
@ -84,8 +85,8 @@ public record StableEnumFunction<E extends Enum<E>, R>(Class<E> enumType,
@Override
public String toString() {
final Collection<Map.Entry<E, StableValueImpl<R>>> entries = new ArrayList<>(delegates.length);
final E[] enumElements = enumType.getEnumConstants();
final Collection<Map.Entry<E, StableValueImpl<R>>> entries = new ArrayList<>(enumElements.length);
int ordinal = firstOrdinal;
for (int i = 0; i < delegates.length; i++, ordinal++) {
if (member.test(ordinal)) {
@ -99,7 +100,7 @@ public record StableEnumFunction<E extends Enum<E>, R>(Class<E> enumType,
public static <T, E extends Enum<E>, R> Function<T, R> of(Set<? extends T> inputs,
Function<? super T, ? extends R> original) {
// The input set is not empty
final Class<E> enumType = (Class<E>)inputs.iterator().next().getClass();
final Class<E> enumType = ((E) inputs.iterator().next()).getDeclaringClass();
final BitSet bitSet = new BitSet(enumType.getEnumConstants().length);
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;

View File

@ -32,7 +32,7 @@ import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
// Note: It would be possible to just use `LazyMap::get` with some additional logic
// Note: It would be possible to just use `StableMap::get` with some additional logic
// instead of this class but explicitly providing a class like this provides better
// debug capability, exception handling, and may provide better performance.
/**

View File

@ -31,9 +31,6 @@ import jdk.internal.vm.annotation.Stable;
import java.util.function.IntFunction;
import java.util.function.Supplier;
// Note: It would be possible to just use `LazyList::get` instead of this
// class but explicitly providing a class like this provides better
// debug capability, exception handling, and may provide better performance.
/**
* Implementation of a stable IntFunction.
* <p>

View File

@ -58,7 +58,7 @@ public record StableSupplier<T>(StableValueImpl<T> delegate,
@Override
public String toString() {
final Object t = delegate.wrappedContentAcquire();
final Object t = delegate.wrappedContentsAcquire();
return t == this ? "(this StableSupplier)" : StableValueImpl.renderWrapped(t);
}

View File

@ -47,7 +47,7 @@ public final class StableUtil {
int length) {
final StringJoiner sj = new StringJoiner(", ", "[", "]");
for (int i = 0; i < length; i++) {
final Object value = delegates[i + offset].wrappedContentAcquire();
final Object value = delegates[i + offset].wrappedContentsAcquire();
if (value == self) {
sj.add("(this " + selfName + ")");
} else {
@ -63,10 +63,10 @@ public final class StableUtil {
boolean curly) {
final StringJoiner sj = new StringJoiner(", ", curly ? "{" : "[", curly ? "}" : "]");
for (var e : delegates) {
final Object value = e.getValue().wrappedContentAcquire();
final Object value = e.getValue().wrappedContentsAcquire();
final String valueString;
if (value == self) {
valueString = ("(this ") + selfName + ")";
valueString = "(this " + selfName + ")";
} else {
valueString = StableValueImpl.renderWrapped(value);
}

View File

@ -51,10 +51,10 @@ public final class StableValueImpl<T> implements StableValue<T> {
// Unsafe offsets for direct field access
private static final long CONTENT_OFFSET =
private static final long CONTENTS_OFFSET =
UNSAFE.objectFieldOffset(StableValueImpl.class, "contents");
// Used to indicate a holder value is `null` (see field `value` below)
// A wrapper method `nullSentinel()` is used for generic type conversion.
// Used to indicate a holder value is `null` (see field `contents` below)
private static final Object NULL_SENTINEL = new Object();
// Generally, fields annotated with `@Stable` are accessed by the JVM using special
@ -77,13 +77,13 @@ public final class StableValueImpl<T> implements StableValue<T> {
@ForceInline
@Override
public boolean trySet(T contents) {
if (wrappedContentAcquire() != null) {
if (wrappedContentsAcquire() != null) {
return false;
}
// Prevent reentry via an orElseSet(supplier)
preventReentry();
// Mutual exclusion is required here as `orElseSet` might also
// attempt to modify the `wrappedValue`
// attempt to modify `this.contents`
synchronized (this) {
return wrapAndSet(contents);
}
@ -102,7 +102,7 @@ public final class StableValueImpl<T> implements StableValue<T> {
@ForceInline
@Override
public T orElseThrow() {
final Object t = wrappedContentAcquire();
final Object t = wrappedContentsAcquire();
if (t == null) {
throw new NoSuchElementException("No contents set");
}
@ -112,21 +112,21 @@ public final class StableValueImpl<T> implements StableValue<T> {
@ForceInline
@Override
public T orElse(T other) {
final Object t = wrappedContentAcquire();
final Object t = wrappedContentsAcquire();
return (t == null) ? other : unwrap(t);
}
@ForceInline
@Override
public boolean isSet() {
return wrappedContentAcquire() != null;
return wrappedContentsAcquire() != null;
}
@ForceInline
@Override
public T orElseSet(Supplier<? extends T> supplier) {
Objects.requireNonNull(supplier);
final Object t = wrappedContentAcquire();
final Object t = wrappedContentsAcquire();
return (t == null) ? orElseSetSlowPath(supplier) : unwrap(t);
}
@ -149,7 +149,7 @@ public final class StableValueImpl<T> implements StableValue<T> {
@Override
public String toString() {
final Object t = wrappedContentAcquire();
final Object t = wrappedContentsAcquire();
return t == this
? "(this StableValue)"
: renderWrapped(t);
@ -158,8 +158,8 @@ public final class StableValueImpl<T> implements StableValue<T> {
// Internal methods shared with other internal classes
@ForceInline
public Object wrappedContentAcquire() {
return UNSAFE.getReferenceAcquire(this, CONTENT_OFFSET);
public Object wrappedContentsAcquire() {
return UNSAFE.getReferenceAcquire(this, CONTENTS_OFFSET);
}
static String renderWrapped(Object t) {
@ -185,11 +185,11 @@ public final class StableValueImpl<T> implements StableValue<T> {
* @return if the contents was set
*/
@ForceInline
private boolean wrapAndSet(Object newValue) {
private boolean wrapAndSet(T newValue) {
assert Thread.holdsLock(this);
// We know we hold the monitor here so plain semantic is enough
if (contents == null) {
UNSAFE.putReferenceRelease(this, CONTENT_OFFSET, wrap(newValue));
UNSAFE.putReferenceRelease(this, CONTENTS_OFFSET, wrap(newValue));
return true;
}
return false;

View File

@ -52,7 +52,13 @@ final class StableFunctionTest {
ZERO(0),
ILLEGAL_BEFORE(-1),
// Valid values
THIRTEEN(13),
THIRTEEN(13) {
@Override
public String toString() {
// getEnumConstants will be `null` for this enum as it is overridden
return super.toString()+" (Overridden)";
}
},
ILLEGAL_BETWEEN(-2),
FORTY_TWO(42),
// Illegal values (not in the input set)
@ -196,6 +202,13 @@ final class StableFunctionTest {
assertEquals("jdk.internal.lang.stable.StableFunction", emptyFunction.getClass().getName());
}
@Test
void overriddenEnum() {
final var overridden = Value.THIRTEEN;
Function<Value, Integer> enumFunction = StableValue.function(EnumSet.of(overridden), Value::asInt);
assertEquals(MAPPER.apply(overridden), enumFunction.apply(overridden));
}
private static Stream<Set<Value>> nonEmptySets() {
return Stream.of(
Set.of(Value.FORTY_TWO, Value.THIRTEEN),

View File

@ -35,7 +35,6 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
@ -46,7 +45,9 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@ -242,18 +243,8 @@ final class StableListTest {
assertEquals(eq.toString(), lazy.toString());
}
@Test
void subListToString() {
subListToString0(newList());
subListToString0(newList().subList(1, SIZE));
subListToString0(newList().subList(1, SIZE).subList(0, SIZE - 2));
}
void subListToString0(List<Integer> subList) {
void assertUnevaluated(List<Integer> subList) {
assertEquals(asString(".unset", subList), subList.toString());
var first = subList.getFirst();
assertEquals(asString(first.toString(), subList), subList.toString());
}
@Test
@ -272,17 +263,47 @@ final class StableListTest {
}
@Test
void reversedToString() {
var reversed = newList().reversed();
subListToString0(reversed);
void sublistReversedToString() {
var actual = StableValue.list(4, IDENTITY);
var expected = List.of(0, 1, 2, 3);
for (UnaryOperation op : List.of(
new UnaryOperation("subList", l -> l.subList(1, 3)),
new UnaryOperation("reversed", List::reversed))) {
actual = op.apply(actual);
expected = op.apply(expected);
}
// Touch one of the elements
actual.getLast();
var actualToString = actual.toString();
var expectedToString = expected.toString().replace("2", ".unset");
assertEquals(expectedToString, actualToString);
}
// This test makes sure successive view operations retains the property
// of being a Stable view.
@Test
void subListReversedToString() {
var list = newList().subList(1, SIZE - 1).reversed();
// This combination is not lazy. There has to be a limit somewhere.
var regularList = newRegularList().subList(1, SIZE - 1).reversed();
assertEquals(regularList.toString(), list.toString());
void viewsStable() {
viewOperations().forEach(op0 -> {
viewOperations().forEach( op1 -> {
viewOperations().forEach(op2 -> {
var list = newList();
var view1 = op0.apply(list);
var view2 = op1.apply(view1);
var view3 = op2.apply(view2);
var className3 = className(view3);
var transitions = className(list) + ", " +
op0 + " -> " + className(view1) + ", " +
op1 + " -> " + className(view2) + ", " +
op2 + " -> " + className3;
assertTrue(className3.contains("Stable"), transitions);
assertUnevaluated(list);
assertUnevaluated(view1);
assertUnevaluated(view2);
assertUnevaluated(view3);
});
});
});
}
@Test
@ -294,6 +315,23 @@ final class StableListTest {
assertEquals("Recursive initialization of a stable value is illegal", x.getMessage());
}
@Test
void indexOfNullInViews() {
final int size = 5;
final int middle = 2;
viewOperations().forEach(op0 -> {
viewOperations().forEach( op1 -> {
viewOperations().forEach(op2 -> {
var list = StableValue.list(size, x -> x == middle ? null : x);;
var view1 = op0.apply(list);
var view2 = op1.apply(view1);
var view3 = op2.apply(view2);
assertEquals(middle, view3.indexOf(null));
});
});
});
}
// Immutability
@ParameterizedTest
@ -362,6 +400,39 @@ final class StableListTest {
assertEquals(SIZE, idMap.size());
}
@Test
void childObjectOpsLazy() {
viewOperations().forEach(op0 -> {
viewOperations().forEach(op1 -> {
viewOperations().forEach(op2 -> {
childOperations().forEach(co -> {
var list = newList();
var view1 = op0.apply(list);
var view2 = op1.apply(view1);
var view3 = op2.apply(view2);
var child = co.apply(view3);
var childClassName = className(child);
var transitions = className(list) + ", " +
op0 + " -> " + className(view1) + ", " +
op1 + " -> " + className(view2) + ", " +
op2 + " -> " + className(view3) + ", " +
co + " -> " + childClassName;
// None of these operations should trigger evaluation
var childToString = child.toString();
int childHashCode = child.hashCode();
boolean childEqualToNewObj = child.equals(new Object());
assertUnevaluated(list);
assertUnevaluated(view1);
assertUnevaluated(view2);
assertUnevaluated(view3);
});
});
});
});
}
// Support constructs
record Operation(String name,
@ -370,6 +441,36 @@ final class StableListTest {
@Override public String toString() { return name; }
}
record UnaryOperation(String name,
UnaryOperator<List<Integer>> operator) implements UnaryOperator<List<Integer>> {
@Override public List<Integer> apply(List<Integer> list) { return operator.apply(list); }
@Override public String toString() { return name; }
}
record ListFunction(String name,
Function<List<Integer>, Object> function) implements Function<List<Integer>, Object> {
@Override public Object apply(List<Integer> list) { return function.apply(list); }
@Override public String toString() { return name; }
}
static Stream<UnaryOperation> viewOperations() {
return Stream.of(
// We need identity to capture all combinations
new UnaryOperation("identity", l -> l),
new UnaryOperation("reversed", List::reversed),
new UnaryOperation("subList", l -> l.subList(0, l.size()))
);
}
static Stream<ListFunction> childOperations() {
return Stream.of(
// We need identity to capture all combinations
new ListFunction("iterator", List::iterator),
new ListFunction("listIterator", List::listIterator),
new ListFunction("listIterator", List::stream)
);
}
static Stream<Operation> nullAverseOperations() {
return Stream.of(
new Operation("forEach", l -> l.forEach(null)),
@ -433,4 +534,7 @@ final class StableListTest {
.collect(Collectors.joining(", ")) + "]";
}
static String className(Object o) {
return o.getClass().getName();
}
}

View File

@ -248,6 +248,37 @@ final class StableMapTest {
assertEquals(KEYS, encountered);
}
@Test
void stableEntry() {
var map = newMap();
var entry = map.entrySet().stream()
.filter(e -> e.getKey().equals(KEY))
.findAny()
.orElseThrow();
assertEquals(KEY + "=.unset", entry.toString());
var otherDifferent = Map.entry(-1, -1);
assertNotEquals(entry, otherDifferent);
assertEquals(KEY + "=.unset", entry.toString());
var otherEqual = Map.entry(KEY, KEY);
assertEquals(entry, otherEqual);
assertEquals(KEY + "=" + KEY, entry.toString());
assertEquals(entry.hashCode(), otherEqual.hashCode());
}
@Test
void stableForEachEntry() {
var map = newMap();
// Only touch the key.
map.entrySet().iterator().forEachRemaining(Map.Entry::getKey);
map.entrySet().iterator()
.forEachRemaining(e -> assertTrue(e.toString().contains(".unset")));
// Only touch the value.
map.entrySet().iterator().forEachRemaining(Map.Entry::getValue);
map.entrySet().iterator()
.forEachRemaining(e -> assertFalse(e.toString().contains(".unset")));
}
// Immutability
@ParameterizedTest
@MethodSource("unsupportedOperations")

View File

@ -29,6 +29,7 @@
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@ -355,12 +356,14 @@ final class StableValueTest {
void race(BiPredicate<StableValue<Integer>, Integer> winnerPredicate) {
int noThreads = 10;
CountDownLatch starter = new CountDownLatch(1);
CountDownLatch starter = new CountDownLatch(noThreads);
StableValue<Integer> stable = StableValue.of();
Map<Integer, Boolean> winners = new ConcurrentHashMap<>();
List<Thread> threads = IntStream.range(0, noThreads).mapToObj(i -> new Thread(() -> {
try {
// Ready, set ...
// Ready ...
starter.countDown();
// ... set ...
starter.await();
// Here we go!
winners.put(i, winnerPredicate.test(stable, i));
@ -370,9 +373,6 @@ final class StableValueTest {
}))
.toList();
threads.forEach(Thread::start);
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
// Start the race
starter.countDown();
threads.forEach(StableValueTest::join);
// There can only be one winner
assertEquals(1, winners.values().stream().filter(b -> b).count());

View File

@ -65,7 +65,8 @@ public class StableMethodHandleBenchmark {
private static final MethodHandle FINAL_MH = identityHandle();
private static final StableValue<MethodHandle> STABLE_MH;
private static MethodHandle mh = identityHandle();
private static /* intentionally not final */ MethodHandle mh = identityHandle();
private static final Dcl<MethodHandle> DCL = new Dcl<>(StableMethodHandleBenchmark::identityHandle);
private static final AtomicReference<MethodHandle> ATOMIC_REFERENCE = new AtomicReference<>(identityHandle());
private static final Map<String, MethodHandle> MAP = new ConcurrentHashMap<>();
@ -112,14 +113,6 @@ public class StableMethodHandleBenchmark {
return (int) STABLE_MH.orElseThrow().invokeExact(1);
}
Object cp() {
CodeBuilder cob = null;
ConstantPoolBuilder cp = ConstantPoolBuilder.of();
cob.ldc(cp.constantDynamicEntry(cp.bsmEntry(cp.methodHandleEntry(BSM_CLASS_DATA), List.of()),
cp.nameAndTypeEntry(DEFAULT_NAME, CD_MethodHandle)));
return null;
}
static MethodHandle identityHandle() {
var lookup = MethodHandles.lookup();
try {