Skip to content
Draft
Show file tree
Hide file tree
Changes from 28 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: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,3 @@
# /lib/
/lib/java
/lib/java-contrib

Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// 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")
public final class PersistentBalancingDoubleListDeque<T> implements PersistentDeque<T> {
final PersistentLinkedList<T> top;
final PersistentLinkedList<T> bottom;

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

private PersistentBalancingDoubleListDeque(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 {
return top.head();
} catch (IllegalStateException e1) {
try {
return bottom.head();
} catch (IllegalStateException e2) {
return null;
}
}
}

/**
* 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 PersistentBalancingDoubleListDeque<T> insertTop(T value) {
return new PersistentBalancingDoubleListDeque<>(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 PersistentBalancingDoubleListDeque<T> insertBottom(T value) {
return new PersistentBalancingDoubleListDeque<>(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 PersistentBalancingDoubleListDeque<T> deleteTop() {
// top should only ever be empty if only one element in bottom or deque completely empty
try {
if (top.isEmpty()) {
return new PersistentBalancingDoubleListDeque<>(top, bottom.tail()).rebalanceDeque();
}
return new PersistentBalancingDoubleListDeque<>(top.tail(), bottom).rebalanceDeque();
} catch (IllegalStateException e) {
return this;
}
}

/**
* Removes element at the bottom of the deque from deque.
*
* @return deque instance after bottom element has been removed
*/
@Override
public PersistentBalancingDoubleListDeque<T> deleteBottom() {
// bottom should only ever be empty if only one element in top or deque completely empty
try {
if (bottom.isEmpty()) {
return new PersistentBalancingDoubleListDeque<>(top.tail(), bottom).rebalanceDeque();
}
return new PersistentBalancingDoubleListDeque<>(top, bottom.tail()).rebalanceDeque();
} catch (IllegalStateException e) {
return this;
}
}

private PersistentBalancingDoubleListDeque<T> rebalanceDeque() {
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 PersistentBalancingDoubleListDeque<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 PersistentBalancingDoubleListDeque<>(newTop, newBottom);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// 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 static com.google.common.truth.Truth.assertThat;

import org.junit.Before;
import org.junit.Test;

public class PersistentBalancingDoubleListDequeTest {

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.

As long as this test class only uses methods defined by the PersistentDeque interface, and works purely in a black-box fashion, it can be named PersistentDequeTest.


PersistentBalancingDoubleListDeque<Object> emptyDeque;

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.

I am questioning whether plain empty Objects, and constant "full" (can this even be "full"?) and size 1 Deques are a good idea to test this data-structure in general.

This can be simplified by removing all of the class variables, as they seem like they can be set up quickly and targeted in actual tests.

Also, using proper, established types instead of plain Objects should be highly considered!

PersistentBalancingDoubleListDeque<Object> size1Deque;
PersistentBalancingDoubleListDeque<Object> fullDeque;
Object o1;
Object o2;
Object o3;
Object o4;
Object o5;

@Before
public void setup() {

emptyDeque = new PersistentBalancingDoubleListDeque<>();

size1Deque = new PersistentBalancingDoubleListDeque<>();
o5 = new Object();
size1Deque = size1Deque.insertTop(o5);

fullDeque = new PersistentBalancingDoubleListDeque<>();
o1 = new Object();
o2 = new Object();
o3 = new Object();
o4 = new Object();
fullDeque = fullDeque.insertTop(o2);
fullDeque = fullDeque.insertTop(o1);
fullDeque = fullDeque.insertBottom(o3);
fullDeque = fullDeque.insertBottom(o4);
}

@Test
public void testEmptyDeque() {
assertThat(emptyDeque.isEmpty()).isTrue();

assertThat(emptyDeque.getTop()).isNull();
assertThat(emptyDeque.getBottom()).isNull();
}

@Test
public void testInsert() {
assertThat(emptyDeque.isEmpty()).isTrue();

emptyDeque = emptyDeque.insertTop(o2);
emptyDeque = emptyDeque.insertTop(o1);
emptyDeque = emptyDeque.insertBottom(o3);
emptyDeque = emptyDeque.insertBottom(o4);

assertThat(emptyDeque.top).containsExactly(o1, o2).inOrder();
assertThat(emptyDeque.bottom).containsExactly(o4, o3).inOrder();
}

@Test
public void testFullDeque() {
assertThat(fullDeque.isEmpty()).isFalse();

assertThat(fullDeque.getTop()).isEqualTo(o1);
assertThat(fullDeque.getBottom()).isEqualTo(o4);
}

@Test
public void testRemove() {
assertThat(fullDeque.isEmpty()).isFalse();

fullDeque = fullDeque.deleteTop();
assertThat(fullDeque.top).containsExactly(o2);
assertThat(fullDeque.bottom).containsExactly(o4, o3).inOrder();

fullDeque = fullDeque.deleteTop();
assertThat(fullDeque.top).containsExactly(o3);
assertThat(fullDeque.bottom).containsExactly(o4);

fullDeque = fullDeque.deleteBottom();
assertThat(fullDeque.top).containsExactly(o3);

fullDeque = fullDeque.deleteBottom();
assertThat(fullDeque.isEmpty()).isTrue();
}

@Test
public void testDequeOfSize1() {
assertThat(size1Deque.isEmpty()).isFalse();

assertThat(size1Deque.getTop()).isEqualTo(o5);
assertThat(size1Deque.getBottom()).isEqualTo(o5);
}
}
74 changes: 74 additions & 0 deletions src/org/sosy_lab/common/collect/PersistentDeque.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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")

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 interface PersistentDeque<T> {

/**
* 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
*/
PersistentBalancingDoubleListDeque<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
*/
PersistentBalancingDoubleListDeque<T> insertBottom(T value);

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

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