kuilt Help

TwoPhaseSet

A set where removal is final. Elements can be added and removed, but once removed they can never come back. When an add and a remove happen at the same time, remove wins.

Converges to: the set of elements that have been added and not yet tombstoned, where tombstones are permanent.

Merge rule

A TwoPhaseSet maintains two GSets: an add-set A and a tombstone-set R. An element is present if element ∈ A ∧ element ∉ R. Merge is union of both sets independently. Because tombstones only grow, a tombstoned element remains absent in all future states — even if another replica concurrently adds it.

Code examples

Add and remove:

@Test fun addThenContains() { val s = TwoPhaseSet.empty<String>().let { it.piece(it.add("x")) } assertTrue(s.contains("x")) }
@Test fun removeMakesAbsent() { val s = TwoPhaseSet.empty<String>().let { it.piece(it.add("x")) } val gone = s.piece(s.remove("x")) assertFalse(gone.contains("x")) }

Remove wins over concurrent re-add:

@Test fun removeWinsOverConcurrentReAdd() { // start: "x" added. Alice removes; Bob (unaware) re-adds. val start = TwoPhaseSet.empty<String>().let { it.piece(it.add("x")) } val alice = start.piece(start.remove("x")) val bob = start.piece(start.add("x")) // The merge: tombstone wins forever. "x" stays gone. assertFalse(alice.piece(bob).contains("x")) }

Tombstone is permanent — resurrection is impossible:

@Test fun cannotResurrectARemovedElement() { // Even on a later isolated add — tombstone permanence is the contract. val s = TwoPhaseSet.empty<String>() .let { it.piece(it.add("x")) } .let { it.piece(it.remove("x")) } val retried = s.piece(s.add("x")) assertFalse(retried.contains("x")) }

When to use

TwoPhaseSet is appropriate when removal semantics must be final — expired tokens, retired identifiers, blacklisted entries. For a set where removed elements can be re-added, use ORSet.

Last modified: 22 June 2026