Skip to content
Draft
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
eda35d3
Add .idea directory to gitignore
Apr 16, 2026
4f14869
Add PersistentDeque class and PersistentDequeInterface
Apr 16, 2026
4690c40
Define own requirements for PersistentDequeInterface and remove 'exte…
Apr 18, 2026
d591655
Implement variables, constructors and isEmpty() in PersistentDeque
Apr 18, 2026
f186772
Implement getTop() in PersistentDeque and fix minor mistakes
Apr 18, 2026
a28145e
Implement getBottom() in PersistentDeque
Apr 18, 2026
b5a5810
Implement insertTop() in PersistentDeque
Apr 18, 2026
afb6bca
Implement insertBottom() in PersistentDeque
Apr 18, 2026
cd9b78f
Implement deleteTop() in PersistentDeque
Apr 18, 2026
4cec676
Implement deleteBottom() in PersistentDeque
Apr 18, 2026
75c8153
Add createEmptyPersistentDeque() to PersistentDeque
Apr 18, 2026
7d14d39
Add methods for rebalancing deque to PersistentDeque, i.e. when one o…
Apr 18, 2026
0a981a7
Return current Deque unchanged when trying to rebalance Deque with to…
Apr 18, 2026
961b774
Rebalance new Deque before returning it in deleteTop() and deleteBott…
Apr 18, 2026
7234cb6
Cause split() to throw exception on attempt to split empty list in Pe…
Apr 18, 2026
c4604d4
Provide fixes for compile errors in PersistentDeque
Apr 18, 2026
9be3445
Add documentation to PersistentDeque
Apr 18, 2026
e09ac80
Add documentation to PersistentDequeInterface
Apr 18, 2026
30e0c25
Add checkstyle fixes to PersistentDeque and PersistentDequeInterface
Apr 18, 2026
7c55bf8
Handle case when deque contains one element and requesting first or l…
Apr 19, 2026
15f7b96
Add comments for code legibility in PersistentDeque
Apr 19, 2026
b7a8a7f
Add test suite for PersistentDeque
Apr 19, 2026
f5d4840
Conform to format-source
Apr 19, 2026
0e4dfd7
Fix bug exposed by tests in PersistentDeque
Apr 19, 2026
c668e2c
Alter tests in PersistentDequeTest to use assertThat()...
Apr 19, 2026
8d9b31d
Remove change to gitignore
Apr 21, 2026
ca2f3ff
Merge remote-tracking branch 'refs/remotes/origin/main' into 48-exten…
Apr 21, 2026
153c25b
Rename for clarity
Apr 21, 2026
9bceebb
Make PersistentDeque extend java.util.Deque
Apr 23, 2026
befc5a1
Specify immutable versions of mutable methods from java.util.Deque in…
Apr 23, 2026
58be67a
Implement methods for static and non-static creation of an empty dequ…
Apr 23, 2026
191e30c
Replace previous methods with new versions specified by recent change…
Apr 23, 2026
3317bc4
Merge branch 'main' into 48-extend-persistent-data-structures-with-deque
Apr 23, 2026
d11eaa8
Add missing method specifications to PersistentDeque
Apr 23, 2026
25ae5c5
Add AbstractImmutableDeque
Apr 23, 2026
eb9ccbf
Add implementation for most methods from PersistentDeque in Persisten…
Apr 23, 2026
94dd4e9
Add optional and unsupported methods inherited from java.util.Collect…
Apr 23, 2026
5dce0db
Add implementation of copyAndRemoveFirstOccurrence and copyAndRemoveL…
Apr 24, 2026
f34e96d
Remove methods in PersistentDeque that are inherited from super as is
Apr 28, 2026
8723ee4
Remove unnecessary try/catch in PersistentBalancingDoubleListDeque
Apr 28, 2026
31267c8
Remove more unnecessary try/catch in PersistentBalancingDoubleListDeque
Apr 28, 2026
bb00c3d
Rename test class to PersistentDequeTest and refactor methods
Apr 28, 2026
8420b86
Add comment to document invariant necessitating the methods rebalance…
Apr 28, 2026
8934375
Add nested DequeIterator class and implementation of iterator() and d…
Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@
/lib/java
/lib/java-contrib

/.idea/
Comment thread
baierd marked this conversation as resolved.
Outdated
190 changes: 190 additions & 0 deletions src/org/sosy_lab/common/collect/PersistentDeque.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// This file is part of SoSy-Lab Common,
// a library of useful utilities:
// https://github.com/sosy-lab/java-common-lib
//
// SPDX-FileCopyrightText: 2026 Dirk Beyer <https://www.sosy-lab.org>
//
// SPDX-License-Identifier: Apache-2.0

package org.sosy_lab.common.collect;

import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.Var;
import java.util.Iterator;
import javax.annotation.Nullable;

/**
* A persistent implementation of a deque on the basis of {@link PersistentLinkedList}.
*
* <p>To avoid O(n) runtime complexity when accessing the bottom of the deque, two separate
* {@link PersistentLinkedList}s are used. {@code top} represents the top part of the deque,
* while {@code bottom} forms the lower part of the deque. If one were to reverse {@code bottom}
* and then add it to the bottom of {@code top}, one would receive one list representing the
* whole deque in correct order.
*
* <p>It provides operations to show the top- and bottom-most elements of the deque, as well as
* ones
* to remove them or add new items to the deque in either places.
* In most cases, these will complete in O(1). Occasionally, these operations will require more
* time, as the deque might need to be rebalanced (i.e. when one of the lists becomes empty, the
* other list is split up into top and bottom to further guarantee access to both ends of
* the deque).
*
* <p>Currently, it is only possible to create an empty deque and then add new elements one
* at a
* time.
*
* @param <T> type of elements to be stored in deque
*/
@Immutable(containerOf = "T")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The class is declared to be immutable, is actually immutable, but it is still using/allowing mutable methods.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You don't need to list methods that are just handed through, like Iterator<T> iterator();.

Overall this looks much better! Thank you!

public final class PersistentDeque<T> implements PersistentDequeInterface<T> {
Comment thread
baierd marked this conversation as resolved.
Outdated
final PersistentLinkedList<T> top;
final PersistentLinkedList<T> bottom;

public PersistentDeque() {
top = PersistentLinkedList.of();
bottom = PersistentLinkedList.of();
}

private PersistentDeque(PersistentLinkedList<T> top, PersistentLinkedList<T> bottom) {
this.top = top;
this.bottom = bottom;
}

/**
* Checks both sublists and returns true if both are empty, false if at least one is not.
*
* @return true if {@code top} and {@code bottom} are both empty, false if at least one is not
*/
@Override
public boolean isEmpty() {
return top.isEmpty() && bottom.isEmpty();
}

/**
* Returns element at the top of the deque.
*
* @return element at top of deque; null if deque empty
*/
@Nullable
@Override
public T getTop() {
//If deque contains only one element, one of the lists will be empty. Calling head() on an
// empty list throws an exception, so this needs to be caught. Further, the one element in
// the non-empty list should be returned, as it is both head and tail of the deque.
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your two try-catch blocks could be simplified by using only a single one.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done.

@baierd baierd Apr 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Control-flow with exceptions should be avoided as much as possible (it is slow, hard to read/debug etc.). Why don't you simply check whether top/bottom are empty before calling head()?

There are multiple occurrences of this behavior in this class.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right, they could have been avoided altogether. I've removed them all and altered the methods accordingly.

return top.head();
} catch(IllegalStateException e1) {
try {
return bottom.head();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are you sure you want the head here in all cases?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I was a bit iffy about that too, but other options seemed bulky. As long as the rebalancing works correctly and is called where necessary, bottom should only contain at most one element if top.head() threw an exception. I will admit though that this logic relies on the rest of the code being sound, or it could cause mistakes...
I will try figure out a nicer way of doing it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK. I'll take a closer look at the re-balancing once the formalities/API renamings etc. are done.

} catch(IllegalStateException e2) {
return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you really want to return null instead of throwing an exception if something goes wrong? (in general)

Try to look at this from a user-perspective; you don't know about the implementation details of a data-structure that you use. You just expect the guarantees given in the documentation. Suddenly you get an unexpected null back (you did not say that null is a possible return value in your interface for getTop()). null is sometimes hard to debug, because if not checked for it, it causes exceptions only when used. But there is no guarantee that the returned value is used right away. It might first be stored in some other data-structure etc. and used much later, making debugging much harder!

You can take a look at how we handle null values (we don't allow them in the first place ;D) here, and Javas Deque implementation shows you how to handle requesting a value for an empty Deque (or data-structure in general).

If an exception is thrown by a used method, it makes sense to just declare the using method as also throwing as long as you can not guarantee that your implementation does never throw this exception. Your documentation should be extended by appropriate reasoning if you also throw the exception (i.e. look at why and when the used method throws)

What might have added to the confusion is that our PersistentLinkedList just throws the wrong exception here ;D

The same applies to getBottom() below.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This should be resolved as all methods returning null do so because they were specified that way in java.util.Deque.

}
}
}

/**
* Returns element at the bottom of the deque.
*
* @return element at bottom of deque; null if deque empty
*/
@Nullable
@Override
public T getBottom() {
//If deque contains only one element, one of the lists will be empty. Calling head() on an
// empty list throws an exception, so this needs to be caught. Further, the one element in
// the non-empty list should be returned, as it is both head and tail of the deque.
try {
return bottom.head();
} catch(IllegalStateException e1) {
try {
return top.head();
} catch(IllegalStateException e2) {
return null;
}
}
}

/**
* Places new element on top of the deque.
*
* @param value element to be added to deque
* @return deque instance with new element added on top
*/
@Override
public PersistentDeque<T> insertTop(T value) {
return new PersistentDeque<>(top.with(value), bottom);
}

/**
* Places new element at the bottom of the deque.
*
* @param value element to be added to deque
* @return deque instance with new element added at the bottom
*/
@Override
public PersistentDeque<T> insertBottom(T value) {
return new PersistentDeque<>(top, bottom.with(value));
}

/**
* Removes element at the top of the deque from deque.
*
* @return deque instance after top element has been removed
*/
@Override
public PersistentDeque<T> deleteTop() {
return new PersistentDeque<>(top.tail(), bottom).rebalanceDeque();
}

/**
* Removes element at the bottom of the deque from deque.
*
* @return deque instance after bottom element has been removed
*/
@Override
public PersistentDeque<T> deleteBottom() {
return new PersistentDeque<>(top, bottom.tail()).rebalanceDeque();
}

private PersistentDeque<T> rebalanceDeque() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It makes sense to document the invariant that has to hold for this method and for the split() method. Also, you can use assertions to actually check them.

boolean topEmpty = top.isEmpty();
boolean bottomEmpty = bottom.isEmpty();

if (topEmpty && bottomEmpty) {
return this;
} else if (topEmpty && !bottomEmpty) {
return split(bottom.reversed());
} else if (!topEmpty && bottomEmpty) {
return split(top);
}

return this;
}

private PersistentDeque<T> split(PersistentLinkedList<T> list) {
int size = list.size();
int halfSize = size / 2;

if (size <= 0) {
throw new IllegalArgumentException("Cannot split empty list!");
} else if (size == 1) {
return this;
}

@Var PersistentLinkedList<T> newTop = PersistentLinkedList.of();
@Var PersistentLinkedList<T> newBottom = PersistentLinkedList.of();
Iterator<T> iterator = list.iterator();

for (int i = 0; i < size; i++) {
T element = iterator.next();
if (i < halfSize) {
newTop = newTop.with(element);
} else {
newBottom = newBottom.with(element);
}
}
newTop = newTop.reversed();
return new PersistentDeque<>(newTop, newBottom);
}
}
77 changes: 77 additions & 0 deletions src/org/sosy_lab/common/collect/PersistentDequeInterface.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// This file is part of SoSy-Lab Common,
// a library of useful utilities:
// https://github.com/sosy-lab/java-common-lib
//
// SPDX-FileCopyrightText: 2026 Dirk Beyer <https://www.sosy-lab.org>
//
// SPDX-License-Identifier: Apache-2.0

package org.sosy_lab.common.collect;

import com.google.errorprone.annotations.Immutable;

/**
* Interface for persistent deques. A persistent data structure is immutable, but provides cheap
* * copy-and-write operations. Thus, all write operations
* ({@link #insertTop(Object)}, {@link #insertBottom(Object)}, {@link #deleteTop()},
* {@link #deleteBottom()}) will
* not modify the current
* instance,
* but return a new instance instead.
*
* @param <T> type of elements stored in deque
*/
@Immutable(containerOf = "T")
public interface PersistentDequeInterface<T> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can just inherit the proper Java interface instead of defining everything on your own.

It is also a good idea to split the ideas of immutability and persistence, as it allows us to build immutable and also persistent data-structures independently of each other, but also helps understanding what each interface does, by splitting the concerns.
This is why we have a PersistentMap, and also a AbstractImmutableMap (more on that below). We then inherit from both to implement an immutable and persistent data-structure, for example our PathCopyingPersistentTreeMap.

Normally we would disallow certain methods due to immutability right away using a default implementation in an immutable interface (e.g. add and remove methods) . But we are still in Java 11 here (update to 17 is currently being worked on). So the solution is an abstract class instead. You can take a look at our AbstractImmutableMap for example. You also get a notion on how to use the annotations like @DoNotCall.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You actually had a reasoning why you did not import java.util.Deque in your description. You should still simply inherit java.util.Deque, and if you don't support methods etc., you just throw an UnsupportedOperationException with a small explanation.

Note: you actually should support at least the core methods/concepts in java.util.Deque, like the iterators.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'm not sure if I included in my reasoning that some of the methods required return types that I didn't think matched up with what would make sense for a persistent version. I.e. pop returns the element that was removed, which would leave no way to return the new, updated instance of the deque. Is that one of the cases where you would like it to throw an exception and create a different method that actually works instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That is why we throw for all modifying methods in immutable data-structures. In your version you have the worst of both worlds, they look like they are mutable, but are not.
You can take a look at AbstractImmutableMap for details for example.

Usually we then introduce new methods following the copy-then-modify approach, for example addAndCopy() instead of add(). addAndCopy() then returns a new version of the current data-structure that is also immutable and persistent, with the new element added.
You can take a look at PersistentMap and its implementation for details for example.

Comment thread
baierd marked this conversation as resolved.
Outdated

/**
* Returns true if deque is empty, false if not.
*
* @return true if deque empty, else false
*/
boolean isEmpty();

/**
* Retrieves element at top of deque.
*
* @return element at top of deque
*/
T getTop();

/**
* Retrieves element at bottom of deque.
*
* @return element at bottom of deque
*/
T getBottom();

/**
* Inserts element at top of deque.
*
* @param value element to be inserted
* @return deque instance with new element at top of deque
*/
PersistentDeque<T> insertTop(T value);

/**
* Inserts element at bottom of deque.
*
* @param value element to be inserted
* @return deque instance with new element at bottom of deque
*/
PersistentDeque<T> insertBottom(T value);

/**
* Deletes element currently at top of deque.
*
* @return deque instance after top element has been removed
*/
PersistentDeque<T> deleteTop();

/**
* Deletes element currently at bottom of deque.
*
* @return deque instance after bottom element has been removed
*/
PersistentDeque<T> deleteBottom();
}