diff --git a/src/org/sosy_lab/common/collect/PackageSanityTest.java b/src/org/sosy_lab/common/collect/PackageSanityTest.java index d349efcf9..877ce29e1 100644 --- a/src/org/sosy_lab/common/collect/PackageSanityTest.java +++ b/src/org/sosy_lab/common/collect/PackageSanityTest.java @@ -16,6 +16,8 @@ public class PackageSanityTest extends AbstractPackageSanityTests { { setDistinctValues( PersistentLinkedList.class, PersistentLinkedList.of(), PersistentLinkedList.of("test")); + setDistinctValues( + PersistentLinkedStack.class, PersistentLinkedStack.of(), PersistentLinkedStack.of("test")); @SuppressWarnings("unchecked") OurSortedMap singletonMap = (PathCopyingPersistentTreeMap) diff --git a/src/org/sosy_lab/common/collect/PersistentLinkedStack.java b/src/org/sosy_lab/common/collect/PersistentLinkedStack.java new file mode 100644 index 000000000..a337fd33a --- /dev/null +++ b/src/org/sosy_lab/common/collect/PersistentLinkedStack.java @@ -0,0 +1,316 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2007-2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkPositionIndex; + +import com.google.common.base.Joiner; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Immutable; +import com.google.errorprone.annotations.Var; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serial; +import java.io.Serializable; +import java.util.NoSuchElementException; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A persistent stack. Pushes structurally share the complete previous stack, and pops return the + * existing tail without copying, while leaving the original stack unchanged. Thus {@link + * #pushAndCopy(Object)}, {@link #popAndCopy()}, {@link #peek()}, {@link #empty()}, {@link + * #isEmpty()}, and {@link #size()} run in O(1) time. Iteration, {@link #equals(Object)}, {@link + * #hashCode()}, and {@link #toString()} have O(n) worst-case stack-traversal overhead. When two + * equal-size {@link PersistentLinkedStack} instances share a tail, {@code equals} traverses only + * the k nodes preceding that tail and thus has O(k) traversal overhead. These bounds exclude work + * performed by element methods. + * + *

All structural state is final and correctly constructed, so the immutable stack structure may + * be accessed concurrently without synchronization. Publishing or updating a shared reference to a + * stack version still requires coordination, and compound updates require synchronization or an + * atomic operation. Each traversal has an independent iterator; a single iterator instance has no + * thread-safety guarantee. Values are stored by reference and are not copied or made immutable or + * thread-safe. + * + *

Null values are not supported. + * + *

Serialization: Serialization and deserialization have O(n) stack-traversal overhead and + * use O(n) temporary memory, excluding the processing of element object graphs. A flattened proxy + * stores the logical values in top-to-bottom order instead of serializing the linked nodes. This + * avoids recursive traversal of long stacks and keeps node and cache fields out of the serialized + * form. Deserialization rejects null proxy data and rebuilds the stack from bottom to top while + * validating each value, thereby preserving order and restoring the canonical empty instance. + * Because each stack is flattened independently, distinct, structurally related stacks serialized + * together have their shared non-empty tails reconstructed independently; {@link #popAndCopy()} on + * a deserialized stack nevertheless returns its existing tail. Element object graphs must not + * contain references back to the containing stack because proxy replacement cannot restore such + * cycles. Persisted data remains readable only while the proxy and element serialized forms remain + * compatible. + * + * @param the type of values + */ +@Immutable(containerOf = "T") +public final class PersistentLinkedStack implements PersistentStack { + + @Serial private static final long serialVersionUID = -4286928240765960519L; + + private static final PersistentLinkedStack EMPTY = new PersistentLinkedStack<>(); + + /** The top value, null exactly for the empty singleton. */ + @SuppressWarnings("serial") // writeReplace prevents direct serialization of this field. + private final @Nullable T top; + + /** The linked tail, null exactly for the empty singleton. */ + private final @Nullable PersistentLinkedStack tail; + + /** + * The size, cached for O(1) access. It is zero exactly for the empty stack and otherwise equals + * {@code tail.size + 1}. The cache is one logical 4-byte {@code int}; its actual footprint + * depends on JVM object layout and alignment. It often fits into padding with compressed + * references and 8-byte alignment, but may add an alignment unit otherwise. + */ + private final int size; + + private PersistentLinkedStack() { + top = null; + tail = null; + size = 0; + } + + private PersistentLinkedStack(T pTop, PersistentLinkedStack pTail) { + top = checkNotNull(pTop); + tail = checkNotNull(pTail); + size = pTail.size + 1; + } + + /** Returns an empty stack. */ + @SuppressWarnings("unchecked") + public static PersistentLinkedStack of() { + return (PersistentLinkedStack) EMPTY; + } + + /** + * Returns a stack containing {@code value}. + * + * @throws NullPointerException if {@code value} is null + */ + public static PersistentLinkedStack of(T value) { + return new PersistentLinkedStack<>(value, PersistentLinkedStack.of()); + } + + @Override + public PersistentLinkedStack pushAndCopy(T value) { + return new PersistentLinkedStack<>(value, this); + } + + @Override + public PersistentLinkedStack popAndCopy() { + if (isEmpty()) { + throw new NoSuchElementException(); + } + return checkNotNull(tail); + } + + @Override + public T peek() { + if (isEmpty()) { + throw new NoSuchElementException(); + } + return checkNotNull(top); + } + + @Override + public PersistentLinkedStack empty() { + return of(); + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + public int size() { + return size; + } + + @Override + public Iterable asTopDownIterable() { + return () -> new StackIterator<>(this); + } + + @Override + public ImmutableList copyToList() { + return ImmutableList.copyOf(asTopDownIterable()).reverse(); + } + + @Override + @SuppressWarnings("ReferenceEquality") // Identical tails need no further comparison. + public boolean equals(@Nullable Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof PersistentStack other) || size != other.size()) { + return false; + } + + @Var PersistentStack left = this; + @Var PersistentStack right = other; + while (!left.isEmpty()) { + if (left == right) { + return true; + } + if (!left.peek().equals(right.peek())) { + return false; + } + left = left.popAndCopy(); + right = right.popAndCopy(); + } + return true; + } + + @Override + public int hashCode() { + @Var int hash = 1; + for (T value : asTopDownIterable()) { + hash = 31 * hash + value.hashCode(); + } + return hash; + } + + /** + * Returns the values in top-to-bottom order, separated by {@code ", "} and enclosed in square + * brackets: {@code [top, ..., bottom]}. The empty stack is represented as {@code []}. + */ + @Override + public String toString() { + return "[" + Joiner.on(", ").join(asTopDownIterable()) + "]"; + } + + @Override + public PersistentLinkedStack takeBottom(int count) { + checkPositionIndex(count, size); + if (count == 0) { + return of(); + } + + @Var PersistentLinkedStack result = this; + while (result.size > count) { + result = result.popAndCopy(); + } + return result; + } + + /** + * Returns a stack by pushing the arguments from left to right. The last argument is on top. + * + * @throws NullPointerException if an element or the varargs array is null + */ + @SafeVarargs + public static PersistentLinkedStack of(T first, T second, T... remaining) { + @Var PersistentLinkedStack result = of(first).pushAndCopy(second); + for (T value : remaining) { + result = result.pushAndCopy(value); + } + return result; + } + + /** + * Returns a stack by pushing the elements in iteration order. The last element is on top. + * + * @throws NullPointerException if {@code values} or an element is null + */ + public static PersistentLinkedStack copyOf(Iterable values) { + checkNotNull(values); + @Var PersistentLinkedStack result = of(); + for (T value : values) { + result = result.pushAndCopy(value); + } + return result; + } + + /** Returns a collector that pushes elements in encounter order, with the last element on top. */ + @SuppressWarnings("NoFunctionalReturnType") + public static Collector> toPersistentLinkedStack() { + return Collectors.collectingAndThen( + ImmutableList.toImmutableList(), PersistentLinkedStack::copyOf); + } + + @Serial + private Object writeReplace() { + return new SerializationProxy(this); + } + + @Serial + @SuppressWarnings("unused") // Serialization hook prevents bypassing the proxy. + private void readObject(ObjectInputStream pInputStream) throws InvalidObjectException { + throw new InvalidObjectException("Serialization proxy required"); + } + + /** Flat serialized form containing the logical values in top-to-bottom order. */ + private static final class SerializationProxy implements Serializable { + + @Serial private static final long serialVersionUID = 2702329958583141147L; + + /** Nullable only to model malformed serialized input, which {@link #readResolve()} rejects. */ + @SuppressWarnings("serial") // ObjectOutputStream checks each element graph at runtime. + private final @Nullable Object @Nullable [] values; + + private SerializationProxy(PersistentLinkedStack stack) { + values = new Object[stack.size]; + @Var int index = 0; + for (Object value : stack.asTopDownIterable()) { + values[index] = value; + index++; + } + } + + @Serial + private Object readResolve() throws InvalidObjectException { + @Nullable Object @Nullable [] serializedValues = values; + if (serializedValues == null) { + throw new InvalidObjectException("Stack values must not be null"); + } + + @Var PersistentLinkedStack stack = PersistentLinkedStack.of(); + // The serialized order is top-to-bottom; push in the opposite direction. + for (@Var int index = serializedValues.length - 1; index >= 0; index--) { + @Nullable Object value = serializedValues[index]; + if (value == null) { + throw new InvalidObjectException("Stack values must not contain null"); + } + stack = stack.pushAndCopy(value); + } + return stack; + } + } + + private static final class StackIterator extends AbstractIterator { + + private PersistentLinkedStack remaining; + + private StackIterator(PersistentLinkedStack stack) { + remaining = stack; + } + + @Override + protected @Nullable T computeNext() { + if (remaining.isEmpty()) { + return endOfData(); + } + T result = remaining.peek(); + remaining = remaining.popAndCopy(); + return result; + } + } +} diff --git a/src/org/sosy_lab/common/collect/PersistentLinkedStackTest.java b/src/org/sosy_lab/common/collect/PersistentLinkedStackTest.java new file mode 100644 index 000000000..40acc6767 --- /dev/null +++ b/src/org/sosy_lab/common/collect/PersistentLinkedStackTest.java @@ -0,0 +1,470 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2007-2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.testing.IteratorFeature; +import com.google.common.collect.testing.IteratorTester; +import com.google.common.collect.testing.ListTestSuiteBuilder; +import com.google.common.collect.testing.TestStringListGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.testing.CollectorTester; +import com.google.common.testing.EqualsTester; +import com.google.common.testing.SerializableTester; +import com.google.errorprone.annotations.Immutable; +import com.google.errorprone.annotations.Var; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serial; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Stream; +import junit.framework.JUnit4TestAdapter; +import junit.framework.TestSuite; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.Test; + +public class PersistentLinkedStackTest { + + private static final ImmutableList> INPUTS = + ImmutableList.of( + ImmutableList.of(), + ImmutableList.of("a"), + ImmutableList.of("a", "b", "c"), + ImmutableList.of("", "", "b")); + + public static junit.framework.Test suite() { + TestSuite suite = new TestSuite(); + suite.addTest(new JUnit4TestAdapter(PersistentLinkedStackTest.class)); + suite.addTest( + ListTestSuiteBuilder.using( + new TestStringListGenerator() { + @Override + protected ImmutableList create(String[] pElements) { + @Var PersistentLinkedStack stack = PersistentLinkedStack.of(); + for (String element : pElements) { + stack = stack.pushAndCopy(element); + } + return stack.copyToList(); + } + }) + .named("PersistentLinkedStack.copyToList") + .withFeatures(CollectionFeature.KNOWN_ORDER, CollectionSize.ANY) + .createTestSuite()); + return suite; + } + + @Test + public void testIterator() { + for (ImmutableList input : INPUTS) { + PersistentStack stack = pushAll(input); + Iterable view = stack.asTopDownIterable(); + IteratorTester tester = + new IteratorTester<>( + 5, + IteratorFeature.UNMODIFIABLE, + input.reverse(), + IteratorTester.KnownOrder.KNOWN_ORDER) { + @Override + protected Iterator newTargetIterator() { + return view.iterator(); + } + }; + tester.test(); + tester.testForEachRemaining(); + } + } + + @Test + public void testForEachRemainingBoundaryCases() { + List remaining = new ArrayList<>(); + Iterator iterator = PersistentLinkedStack.of("a").asTopDownIterable().iterator(); + iterator.forEachRemaining(remaining::add); + assertThat(remaining).containsExactly("a"); + + remaining.clear(); + iterator.forEachRemaining(remaining::add); + PersistentLinkedStack.of() + .asTopDownIterable() + .iterator() + .forEachRemaining(remaining::add); + assertThat(remaining).isEmpty(); + } + + @Test + public void testIteratorsAreIndependent() { + Iterable view = pushAll(ImmutableList.of("a", "b")).asTopDownIterable(); + Iterator first = view.iterator(); + Iterator second = view.iterator(); + + assertThat(first.next()).isEqualTo("b"); + assertThat(second.next()).isEqualTo("b"); + assertThat(first.next()).isEqualTo("a"); + assertThat(second.next()).isEqualTo("a"); + } + + @Test + public void testViewsRemainOnOriginalVersion() { + PersistentStack stack = pushAll(ImmutableList.of("a", "b")); + Iterable view = stack.asTopDownIterable(); + List copy = stack.copyToList(); + PersistentStack extended = stack.pushAndCopy("c"); + + assertThat(extended.peek()).isEqualTo("c"); + assertThat(view).containsExactly("b", "a").inOrder(); + assertThat(copy).containsExactly("a", "b").inOrder(); + } + + @Test + public void testPersistentVersions() { + for (ImmutableList input : INPUTS) { + List> versions = new ArrayList<>(); + versions.add(PersistentLinkedStack.of()); + for (String value : input) { + versions.add(versions.get(versions.size() - 1).pushAndCopy(value)); + } + + for (int i = 0; i < versions.size(); i++) { + PersistentStack version = versions.get(i); + assertThat(version.size()).isEqualTo(i); + assertThat(version.isEmpty()).isEqualTo(i == 0); + assertThat(version.asTopDownIterable()) + .containsExactlyElementsIn(input.subList(0, i).reverse()) + .inOrder(); + if (i > 0) { + assertThat(version.peek()).isEqualTo(input.get(i - 1)); + assertThat(version.popAndCopy()).isSameInstanceAs(versions.get(i - 1)); + } + } + } + } + + @Test + public void testBranchesSharePredecessor() { + PersistentStack predecessor = pushAll(ImmutableList.of("bottom", "middle")); + PersistentStack left = predecessor.pushAndCopy("left"); + PersistentStack right = predecessor.pushAndCopy("right"); + + assertThat(left.peek()).isEqualTo("left"); + assertThat(right.peek()).isEqualTo("right"); + assertThat(left.popAndCopy()).isSameInstanceAs(predecessor); + assertThat(right.popAndCopy()).isSameInstanceAs(predecessor); + assertThat(predecessor.asTopDownIterable()).containsExactly("middle", "bottom").inOrder(); + } + + @Test + public void testCanonicalEmpty() { + PersistentStack empty = PersistentLinkedStack.of(); + PersistentStack singleton = PersistentLinkedStack.of("a"); + + assertThat(empty.isEmpty()).isTrue(); + assertThat(empty.size()).isEqualTo(0); + assertThat(PersistentLinkedStack.of()).isSameInstanceAs(empty); + assertThat(empty.empty()).isSameInstanceAs(empty); + assertThat(singleton.empty()).isSameInstanceAs(empty); + assertThat(singleton.popAndCopy()).isSameInstanceAs(empty); + assertThat(PersistentLinkedStack.copyOf(ImmutableList.of())).isSameInstanceAs(empty); + assertThat(Stream.empty().collect(PersistentLinkedStack.toPersistentLinkedStack())) + .isSameInstanceAs(empty); + } + + @Test + public void testEmptyOperationsThrow() { + PersistentStack empty = PersistentLinkedStack.of(); + + assertThrows(NoSuchElementException.class, empty::peek); + assertThrows(NoSuchElementException.class, empty::popAndCopy); + } + + @Test + public void testOfFactories() { + assertThat(PersistentLinkedStack.of("a").asTopDownIterable()).containsExactly("a"); + assertThat(PersistentLinkedStack.of("a", "b").asTopDownIterable()) + .containsExactly("b", "a") + .inOrder(); + assertThat(PersistentLinkedStack.of("a", "b", "c", "d").asTopDownIterable()) + .containsExactly("d", "c", "b", "a") + .inOrder(); + } + + @Test + public void testArrayIsOneElement() { + String[] value = {"a", "b"}; + PersistentStack stack = PersistentLinkedStack.of(value); + + assertThat(stack.size()).isEqualTo(1); + assertThat(stack.peek()).isSameInstanceAs(value); + assertThat(stack.asTopDownIterable().iterator().next()).isSameInstanceAs(value); + } + + @Test + public void testCopyOf() { + for (ImmutableList input : INPUTS) { + PersistentStack stack = PersistentLinkedStack.copyOf(input); + + assertThat(stack.size()).isEqualTo(input.size()); + assertThat(stack.asTopDownIterable()).containsExactlyElementsIn(input.reverse()).inOrder(); + } + } + + @Test + public void testCopyOfDoesNotRetainInput() { + List input = new ArrayList<>(ImmutableList.of("a", "b")); + PersistentStack stack = PersistentLinkedStack.copyOf(input); + input.clear(); + + assertThat(stack.asTopDownIterable()).containsExactly("b", "a").inOrder(); + assertThat(stack.size()).isEqualTo(2); + } + + @Test + public void testCollector() { + CollectorTester> tester = + CollectorTester.of(PersistentLinkedStack.toPersistentLinkedStack()); + for (ImmutableList input : INPUTS) { + tester.expectCollects(pushAll(input), input.toArray(new String[0])); + } + } + + @Test + public void testNulls() { + assertThrows(NullPointerException.class, () -> PersistentLinkedStack.of((String) null)); + assertThrows(NullPointerException.class, () -> PersistentLinkedStack.of(null, "a")); + assertThrows(NullPointerException.class, () -> PersistentLinkedStack.of("a", null)); + assertThrows( + NullPointerException.class, () -> PersistentLinkedStack.of("a", "b", (String[]) null)); + assertThrows(NullPointerException.class, () -> PersistentLinkedStack.copyOf(null)); + assertThrows(NullPointerException.class, () -> PersistentLinkedStack.of().pushAndCopy(null)); + assertThrows(NullPointerException.class, () -> PersistentLinkedStack.of("a").pushAndCopy(null)); + } + + @Test + public void testNullElements() { + assertThrows( + NullPointerException.class, () -> PersistentLinkedStack.of("a", "b", (String) null)); + assertThrows( + NullPointerException.class, + () -> PersistentLinkedStack.copyOf(Arrays.asList("a", null, "b"))); + assertThrows( + NullPointerException.class, + () -> Stream.of("a", null, "b").collect(PersistentLinkedStack.toPersistentLinkedStack())); + } + + @Test + public void testEquals() { + PersistentStack tail = PersistentLinkedStack.of("bottom"); + BigInteger value = new BigInteger("123456789012345678901234567890"); + BigInteger equalValue = new BigInteger("123456789012345678901234567890"); + + new EqualsTester() + .addEqualityGroup(PersistentLinkedStack.of(), new ListStack<>(ImmutableList.of())) + .addEqualityGroup(PersistentLinkedStack.of("a"), new ListStack<>(ImmutableList.of("a"))) + .addEqualityGroup( + tail.pushAndCopy("middle").pushAndCopy("top"), + tail.pushAndCopy("middle").pushAndCopy("top"), + pushAll(ImmutableList.of("bottom", "middle", "top")), + new ListStack<>(ImmutableList.of("top", "middle", "bottom"))) + .addEqualityGroup(tail.pushAndCopy("middle").pushAndCopy("other")) + .addEqualityGroup(tail.pushAndCopy("other").pushAndCopy("top")) + .addEqualityGroup(pushAll(ImmutableList.of("other", "middle", "top"))) + .addEqualityGroup(pushAll(ImmutableList.of("top", "middle", "bottom"))) + .addEqualityGroup(tail.pushAndCopy("top")) + .addEqualityGroup( + pushAll(ImmutableList.of("a", "a")), new ListStack<>(ImmutableList.of("a", "a"))) + .addEqualityGroup( + PersistentLinkedStack.of(value), + PersistentLinkedStack.of(equalValue), + new ListStack<>(ImmutableList.of(equalValue))) + .addEqualityGroup(ImmutableList.of("top", "middle", "bottom")) + .testEquals(); + } + + @Test + public void testToString() { + assertThat(PersistentLinkedStack.of().toString()).isEqualTo("[]"); + assertThat(PersistentLinkedStack.of("a").toString()).isEqualTo("[a]"); + assertThat(pushAll(ImmutableList.of("bottom", "middle", "top")).toString()) + .isEqualTo("[top, middle, bottom]"); + } + + @Test + public void testSerializable() { + for (ImmutableList input : INPUTS) { + PersistentStack stack = pushAll(input); + @Var PersistentStack copy = SerializableTester.reserializeAndAssert(stack); + + assertThat(copy.size()).isEqualTo(input.size()); + for (String value : input.reverse()) { + assertThat(copy.peek()).isEqualTo(value); + copy = copy.popAndCopy(); + } + assertThat(copy).isSameInstanceAs(PersistentLinkedStack.of()); + } + } + + @Test + public void testLongStackSerialization() { + int length = 10_000; + @Var PersistentStack stack = PersistentLinkedStack.of(); + for (int i = 0; i < length; i++) { + stack = stack.pushAndCopy(i); + } + @Var PersistentStack copy = SerializableTester.reserializeAndAssert(stack); + + for (int i = length - 1; i >= 0; i--) { + assertThat(copy.size()).isEqualTo(i + 1); + assertThat(copy.peek()).isEqualTo(i); + copy = copy.popAndCopy(); + } + assertThat(copy).isSameInstanceAs(PersistentLinkedStack.of()); + } + + @Test + public void testSerializationRejectsNonSerializableElement() throws IOException { + try (ObjectOutputStream output = new ObjectOutputStream(new ByteArrayOutputStream())) { + assertThrows( + NotSerializableException.class, + () -> output.writeObject(PersistentLinkedStack.of(new Object()))); + } + } + + @Test + public void testSerializationRejectsNullArray() { + assertThrows(InvalidObjectException.class, () -> reserializeWithProxyValues(null)); + } + + @Test + public void testSerializationRejectsNullElement() { + assertThrows( + InvalidObjectException.class, + () -> reserializeWithProxyValues(new Object[] {"top", null, "bottom"})); + } + + private static PersistentLinkedStack pushAll(Iterable input) { + @Var PersistentLinkedStack stack = PersistentLinkedStack.of(); + for (T value : input) { + stack = stack.pushAndCopy(value); + } + return stack; + } + + // The string-only fixture has exactly one object array: the proxy's element array. + @SuppressWarnings("BanSerializableRead") // Reads only locally generated test data. + private static Object reserializeWithProxyValues(@Nullable Object @Nullable [] values) + throws IOException, ClassNotFoundException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ObjectOutputStream output = + new ObjectOutputStream(bytes) { + { + enableReplaceObject(true); + } + + @Override + protected @Nullable Object replaceObject(Object object) { + return object instanceof Object[] ? values : object; + } + }) { + output.writeObject(PersistentLinkedStack.of("value")); + } + try (ObjectInputStream input = + new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) { + return input.readObject(); + } + } + + /** Independent value-based implementation used only for equality and hash-code tests. */ + @Immutable(containerOf = "T") + private static final class ListStack implements PersistentStack { + + @Serial private static final long serialVersionUID = 1L; + + private final ImmutableList values; + + private ListStack(ImmutableList pValues) { + values = pValues; + } + + @Override + public PersistentStack pushAndCopy(T value) { + return new ListStack<>(ImmutableList.builder().add(value).addAll(values).build()); + } + + @Override + public PersistentStack popAndCopy() { + if (isEmpty()) { + throw new NoSuchElementException(); + } + return new ListStack<>(values.subList(1, values.size())); + } + + @Override + public T peek() { + if (isEmpty()) { + throw new NoSuchElementException(); + } + return values.get(0); + } + + @Override + public PersistentStack empty() { + return new ListStack<>(ImmutableList.of()); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public int size() { + return values.size(); + } + + @SuppressWarnings("PreferredInterfaceType") + @Override + public Iterable asTopDownIterable() { + return values; + } + + @Override + public ImmutableList copyToList() { + return values.reverse(); + } + + @Override + public PersistentStack takeBottom(int count) { + // Currently not needed in tests + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof PersistentStack other + && values.equals(ImmutableList.copyOf(other.asTopDownIterable())); + } + + @Override + public int hashCode() { + return values.hashCode(); + } + } +} diff --git a/src/org/sosy_lab/common/collect/PersistentStack.java b/src/org/sosy_lab/common/collect/PersistentStack.java new file mode 100644 index 000000000..be8b1b2f2 --- /dev/null +++ b/src/org/sosy_lab/common/collect/PersistentStack.java @@ -0,0 +1,116 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2007-2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect; + +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.Immutable; +import java.io.Serializable; +import java.util.List; +import java.util.NoSuchElementException; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Interface for persistent stacks. A persistent data structure is structurally immutable, but + * provides cheap copy-and-write operations. Operations that conceptually modify the stack return + * another stack while leaving the current instance unchanged. + * + *

Implementations are expected to provide {@link #pushAndCopy(Object)}, {@link #popAndCopy()}, + * {@link #peek()}, {@link #empty()}, {@link #isEmpty()}, and {@link #size()} in O(1) time. + * Iteration proceeds from the top of the stack to the bottom. + * + *

Null values are not supported. + * + *

Instances and their views are thread-safe; iterator instances have no thread-safety guarantee. + * Elements are not copied, and their own thread-safety requirements still apply. + * + *

Stacks are serializable when their elements are serializable. + * + *

Values are stored by reference: they are not copied or made immutable or thread-safe. + * + * @param The type of values. + */ +@Immutable(containerOf = "T") +public interface PersistentStack extends Serializable { + + /** + * Returns a stack with {@code value} on top, leaving this stack unchanged. + * + * @throws NullPointerException if {@code value} is null + */ + @CheckReturnValue + PersistentStack pushAndCopy(T value); + + /** + * Returns a stack without this stack's top value, leaving this stack unchanged. + * + * @throws NoSuchElementException if this stack is empty + */ + @CheckReturnValue + PersistentStack popAndCopy(); + + /** + * Returns this stack's top value without modifying the stack. + * + * @throws NoSuchElementException if this stack is empty + */ + T peek(); + + /** Returns an empty stack of the same implementation. */ + @CheckReturnValue + PersistentStack empty(); + + /** Returns whether this stack contains no values. */ + boolean isEmpty(); + + /** Returns the number of values in this stack. */ + int size(); + + /** + * Returns an unmodifiable top-to-bottom view in O(1) time. Each iterator traverses this stack + * version independently. + */ + Iterable asTopDownIterable(); + + /** Returns an unmodifiable bottom-to-top list in O(n) time and space. */ + List copyToList(); + + /** + * Returns a stack containing the bottom {@code count} elements. + * + * @throws IndexOutOfBoundsException if {@code count} is outside {@code [0, size()]} + */ + @CheckReturnValue + PersistentStack takeBottom(int count); + + /** + * Returns {@code true} if and only if {@code obj} is a {@link PersistentStack} with the same + * number of elements and equal corresponding elements in top-to-bottom order. Elements are + * compared using {@link Object#equals(Object)}. + * + *

Equality is independent of the concrete implementation and structural sharing. All empty + * stacks are equal. + * + * @param obj the object to compare with this stack + * @return whether the object is equal to this stack + */ + @Override + boolean equals(@Nullable Object obj); + + /** + * Returns the hash code of this stack. + * + *

The hash code is computed starting with {@code hash = 1} and applying {@code hash = 31 * + * hash + element.hashCode()} to each element in top-to-bottom order, using Java {@code int} + * arithmetic. The hash code of an empty stack is {@code 1}. + * + * @return the hash code of this stack + */ + @Override + int hashCode(); +}