Skip to content

Add a (copy-)efficient Persistent Stack Implementation - #65

Open
baierd wants to merge 4 commits into
mainfrom
feature/persistent-stack
Open

Add a (copy-)efficient Persistent Stack Implementation#65
baierd wants to merge 4 commits into
mainfrom
feature/persistent-stack

Conversation

@baierd

@baierd baierd commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

We have a PersistentStack implementation in CPAchecker, but not in this library. It makes sense to move it here.

While doing so, the following was added:

  • a interface for persistent stacks PersistentStack.
  • a test class PersistentLinkedStackTest.
  • a extended PersistentLinkedStack implementation that provides serialization through a proxy and an iterator + proper documentation.

Note: the current state is a hybrid of our PersistentStack in CPAchecker and PersistentLinkedList in this repo, but extended using LLM generated code. I reviewed the changes done by the LLM before opening this PR. I am unsure about the serialization though, so input would be appreciated.

@baierd baierd self-assigned this Sep 7, 2026
* @param <T> The type of values.
*/
@Immutable(containerOf = "T")
public interface PersistentStack<T> extends Iterable<T>, Serializable {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think it is better to not extend Iterable in PersistentStack. I personally find it always confusing what the iteration order of a stack is, and would never be sure without looking it up, also because of inconsistency between related classes: For PersistentStack, the only meaningful iteration order is top-down (last element pushed is first). The (de facto deprecated and unused, but still existing) java.util.Stack has a bottom-up iteration order, because it is based on a list and pushes to the end. The standard stack in Java (java.util.Deque) iterates top-down if you use push(), but bottom-up if you use add().

We can instead provide a O(1) method asReverseIterable or so that returns an Iterable (or likely even a SequencedCollection if not a List) view. The name would make it clear that the iteration order is the reverse of the addition order. Or maybe asTopDownIterable() even?
Additionally we can provide a O(n) copyToList() that returns the elements in addition (bottom-up) order.

Comment on lines +27 to +46
* <p>Implementations support standard Java Object Serialization. Serialization succeeds only if
* each contained value and its serialized object graph are serializable at runtime; otherwise,
* serialization fails according to the standard rules, for example with {@link
* java.io.NotSerializableException}.
*
* <p>This serialization contract applies to conforming Java SE runtimes. GraalVM in JVM mode uses
* the same semantics, while GraalVM Native Image may require explicit serialization metadata or
* configuration. Support in non-Java-SE environments, such as Android or GWT, is not guaranteed.
* Deserialization may also be rejected by configured {@link java.io.ObjectInputFilter} policies,
* and portability of serialized data depends on the serialized forms of contained values.
*
* <p>After a stack reference has been made visible to other threads through synchronization, a
* {@code volatile} field, or a concurrency utility, its immutable structure may be accessed
* concurrently. Such coordination is still required to publish or update a shared reference to a
* stack version, and compound updates require synchronization or an atomic operation. No
* thread-safety guarantee is made for iterator instances.
*
* <p>Values are stored by reference: they are not copied or made immutable or thread-safe. Changes
* to mutable values can affect equality and hash codes. Operations that depend on values also
* depend on their thread safety.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, this is all true, but it is also the same for all such collections and pretty standard. I would shorten to just say something like "Serializable if all elements are serializable" and "thread safe including its collection views, but no guarantees for the iterator".

import java.util.NoSuchElementException;
import org.junit.Test;

public class PersistentLinkedStackTest {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests based on Guava's testlib like we have for PathCopyingPersistentTreeMap. These are much more comprehensive than what we would think of.

* @param <T> The type of values.
*/
@Immutable(containerOf = "T")
public interface PersistentStack<T> extends Iterable<T>, Serializable {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is missing in this interface is a definition of equals and hashCode. Typical data structures define these precisely such that even different implementations of the same interface are guaranteed to compare equal. The current equals and hashCode implementations would not achieve this, for example.

}
}

private static final class StackIterator<T> extends UnmodifiableIterator<T> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use AbstractIterator.


@Override
public boolean hasNext() {
return stack != null && !stack.isEmpty();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one of these conditions should be needed. If is confusing if there are two possible different states of the iterator that both mean "end of iteration".

* brackets: {@code [top, ..., bottom]}. The empty stack is represented as {@code []}.
*/
@Override
public String toString() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be implemented trivially using Joiner or Collectors.joining().

* @param <T> the type of values
*/
@Immutable(containerOf = "T")
public final class PersistentLinkedStack<T> implements PersistentStack<T> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also want factory methods, which then can be used for deserialization.

And for example a Collector implementation is also missing.

@PhilippWendler

Copy link
Copy Markdown
Member

We can consider adding some bulk operations, for example a method to get the bottom n elements, similar to sublist(), and maybe more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants