From c2985894c195baac7b574b9894995707ecbf6bc6 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Wed, 29 Jan 2025 19:05:53 +0100 Subject: [PATCH] Started work on Rxn.unsafe.unread --- .../main/scala/dev/tauri/choam/core/Rxn.scala | 20 +++ .../tauri/choam/internal/mcas/WdLike.scala | 3 + .../tauri/choam/internal/mcas/WdLike.scala | 3 + .../choam/internal/mcas/AbstractHamt.scala | 116 ++++++++-------- .../dev/tauri/choam/internal/mcas/Hamt.scala | 125 ++++++++++++------ .../tauri/choam/internal/mcas/HamtSpec.scala | 3 + 6 files changed, 168 insertions(+), 102 deletions(-) diff --git a/core/shared/src/main/scala/dev/tauri/choam/core/Rxn.scala b/core/shared/src/main/scala/dev/tauri/choam/core/Rxn.scala index aced046c..b6eee095 100644 --- a/core/shared/src/main/scala/dev/tauri/choam/core/Rxn.scala +++ b/core/shared/src/main/scala/dev/tauri/choam/core/Rxn.scala @@ -509,6 +509,9 @@ object Rxn extends RxnInstances0 { final def ticketRead[A](r: Ref[A]): Axn[unsafe.Ticket[A]] = new TicketRead[A](r.loc) + final def unread[A](r: Ref[A]): Axn[Unit] = + new Unread(r) + private[choam] final def cas[A](r: Ref[A], ov: A, nv: A): Axn[Unit] = new Cas[A](r.loc, ov, nv) @@ -898,6 +901,11 @@ object Rxn extends RxnInstances0 { final override def toString: String = s"OrElse(${left}, ${right})" } + private final class Unread[A](val ref: Ref[A]) extends Rxn[Any, Unit] { + private[core] final override def tag = 32 + final override def toString: String = s"Unread(${ref})" + } + // Interpreter: private[this] final class PostCommitResultMarker // TODO: make this a java enum? @@ -1849,6 +1857,18 @@ object Rxn extends RxnInstances0 { saveStmAlt(c.right) contT.push(RxnConsts.ContOrElse) loop(c.left) + case 32 => // Unread + val c = curr.asInstanceOf[Unread[_]] + val hwd = desc.getOrElseNull(c.ref.loc) + if (hwd ne null) { + if (hwd.readOnly) { + ??? // OK, remove it + } else { + throw new IllegalStateException(s"${c.ref} is not read-only") + } + } // else: not in readset, nothing to do + a = () + loop(next()) case t => // mustn't happen impossible(s"Unknown tag ${t} for ${curr}") } diff --git a/mcas/js/src/main/scala/dev/tauri/choam/internal/mcas/WdLike.scala b/mcas/js/src/main/scala/dev/tauri/choam/internal/mcas/WdLike.scala index cc9af04f..6b1e5146 100644 --- a/mcas/js/src/main/scala/dev/tauri/choam/internal/mcas/WdLike.scala +++ b/mcas/js/src/main/scala/dev/tauri/choam/internal/mcas/WdLike.scala @@ -30,6 +30,9 @@ sealed abstract class WdLike[A] extends Hamt.HasKey[MemoryLocation[A]] { final override def key: MemoryLocation[A] = this.address + + final override def isTomb: Boolean = + false } final class LogEntry[A] private ( // formerly called HWD diff --git a/mcas/jvm/src/main/scala/dev/tauri/choam/internal/mcas/WdLike.scala b/mcas/jvm/src/main/scala/dev/tauri/choam/internal/mcas/WdLike.scala index eafabae7..fa1bb77a 100644 --- a/mcas/jvm/src/main/scala/dev/tauri/choam/internal/mcas/WdLike.scala +++ b/mcas/jvm/src/main/scala/dev/tauri/choam/internal/mcas/WdLike.scala @@ -30,6 +30,9 @@ sealed trait WdLike[A] extends Hamt.HasKey[MemoryLocation[A]] { final override def key: MemoryLocation[A] = this.address + + final override def isTomb: Boolean = + false } // TODO: this is duplicated on JS diff --git a/mcas/shared/src/main/scala/dev/tauri/choam/internal/mcas/AbstractHamt.scala b/mcas/shared/src/main/scala/dev/tauri/choam/internal/mcas/AbstractHamt.scala index 09dc9d0f..91e75bf2 100644 --- a/mcas/shared/src/main/scala/dev/tauri/choam/internal/mcas/AbstractHamt.scala +++ b/mcas/shared/src/main/scala/dev/tauri/choam/internal/mcas/AbstractHamt.scala @@ -103,8 +103,14 @@ private[mcas] abstract class AbstractHamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K] this.arrays(newDepth) = node.contentsArr this.loadNext() case a => - // found it: - this.loadedNext = a.asInstanceOf[V] + val av = a.asInstanceOf[V] + if (av.isTomb) { + // skip empty slot: + this.loadNext() + } else { + // found it: + this.loadedNext = av + } } } else { // ascend: @@ -151,18 +157,21 @@ private[mcas] abstract class AbstractHamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K] arrIdx = unpackSize(arrIdxAndBlue) isBlueSt &= unpackBlue(arrIdxAndBlue) case a => - // temporary assertion to diagnose a bug here: - if (arrIdx >= arr.length) { - throw new AssertionError( - s"indexing array of length ${arr.length} with index ${arrIdx} (" + - s"a = ${a}; arr = ${arr.mkString("[", ", ", "]")}; " + - s"contents = ${contents.mkString("[", ", ", "]")})" - ) + val av = a.asInstanceOf[V] + if (!av.isTomb) { + // temporary assertion to diagnose a bug here: + if (arrIdx >= arr.length) { + throw new AssertionError( + s"indexing array of length ${arr.length} with index ${arrIdx} (" + + s"a = ${a}; arr = ${arr.mkString("[", ", ", "]")}; " + + s"contents = ${contents.mkString("[", ", ", "]")})" + ) + } + // end of temporary assertion + arr(arrIdx) = convertForArray(av, tok, flag = flag) + arrIdx += 1 + isBlueSt &= isBlue(av) } - // end of temporary assertion - arr(arrIdx) = convertForArray(a.asInstanceOf[V], tok, flag = flag) - arrIdx += 1 - isBlueSt &= isBlue(a.asInstanceOf[V]) } i += 1 } @@ -181,7 +190,10 @@ private[mcas] abstract class AbstractHamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K] case node: AbstractHamt[_, _, _, _, _, _] => curr = node.insertIntoHamt(curr) case a => - curr = curr.asInstanceOf[H].insertInternal(a.asInstanceOf[V]) + val av = a.asInstanceOf[V] + if (!av.isTomb) { + curr = curr.asInstanceOf[H].insertInternal(av) + } } i += 1 } @@ -201,8 +213,11 @@ private[mcas] abstract class AbstractHamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K] return false // scalafix:ok } case a => - if (!this.predicateForForAll(a.asInstanceOf[V], tok)) { - return false // scalafix:ok + val av = a.asInstanceOf[V] + if (!av.isTomb) { + if (!this.predicateForForAll(av, tok)) { + return false // scalafix:ok + } } } i += 1 @@ -212,46 +227,24 @@ private[mcas] abstract class AbstractHamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K] } protected def equalsInternal(that: AbstractHamt[_, _, _, _, _, _]): Boolean = { - // Insertions are not order-dependent, and - // there is no deletion, so HAMTs with the - // same values always have the same tree - // structure. So we can just traverse the - // 2 trees at the same time. - val thisContents = this.contentsArr - val thatContents = that.contentsArr - val thisLen = thisContents.length - val thatLen = thatContents.length - if (thisLen == thatLen) { - var i = 0 - while (i < thisLen) { - val iOk = thisContents(i) match { - case null => - thatContents(i) eq null - case thisNode: AbstractHamt[_, _, _, _, _, _] => - thatContents(i) match { - case thatNode: AbstractHamt[_, _, _, _, _, _] => - thisNode.equalsInternal(thatNode) - case _ => // including null - false - } - case thisValue => - thatContents(i) match { - case null | (_: Hamt[_, _, _, _, _, _]) => - false - case thatValue => - thisValue == thatValue - } - } - if (iOk) { - i += 1 - } else { + // Due to tombstones, HAMTs with the same + // values (tombstones are not values) doesn't + // necessarily have the same tree structure. + // However, traversing them should yield the + // same values in the same order. For that, + // we need to use the iterators: + val thisItr = this.valuesIterator + val thatItr = that.valuesIterator + while (thisItr.hasNext) { + if (thatItr.hasNext) { + if (thisItr.next() != thatItr.next()) { return false // scalafix:ok } + } else { + return false // scalafix:ok } - true - } else { - false } + !thatItr.hasNext } protected final def hashCodeInternal(s: Int): Int = { @@ -266,8 +259,11 @@ private[mcas] abstract class AbstractHamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K] case node: AbstractHamt[_, _, _, _, _, _] => curr = node.hashCodeInternal(curr) case a => - curr = MurmurHash3.mix(curr, (a.asInstanceOf[V].key.hash >>> 32).toInt) - curr = MurmurHash3.mix(curr, a.##) + val av = a.asInstanceOf[V] + if (!av.isTomb) { + curr = MurmurHash3.mix(curr, (av.key.hash >>> 32).toInt) + curr = MurmurHash3.mix(curr, a.##) + } } i += 1 } @@ -286,12 +282,14 @@ private[mcas] abstract class AbstractHamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K] case node: AbstractHamt[_, _, _, _, _, _] => fst = node.toStringInternal(sb, fst) case a => - if (!fst) { - sb.append(", ") - } else { - fst = false + if (!a.asInstanceOf[V].isTomb) { + if (!fst) { + sb.append(", ") + } else { + fst = false + } + sb.append(a.toString) } - sb.append(a.toString) } i += 1 } diff --git a/mcas/shared/src/main/scala/dev/tauri/choam/internal/mcas/Hamt.scala b/mcas/shared/src/main/scala/dev/tauri/choam/internal/mcas/Hamt.scala index 5f33bbec..2bd8097c 100644 --- a/mcas/shared/src/main/scala/dev/tauri/choam/internal/mcas/Hamt.scala +++ b/mcas/shared/src/main/scala/dev/tauri/choam/internal/mcas/Hamt.scala @@ -74,7 +74,7 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A /** * Contains 1 bits in exactly the places where the imaginary 64-element - * sparse array has "something" (either a value or a sub-node). + * sparse array has "something" (either a value, a sub-node, or a tombstone). */ private val bitmap: Long, @@ -164,6 +164,16 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A } } + final def removed(k: K): H = { + // TODO: avoid always allocating a new EntryVisitor here: + this.computeOrModify(k, null, new Hamt.EntryVisitor[K, V, Any] { + final override def entryPresent(k: K, v: V, tok: Any): V = + new Hamt.Tombstone(k.hash).asInstanceOf[V] + final override def entryAbsent(k: K, tok: Any): V = + nullOf[V] // OK, nothing to delete + }) + } + /** * Converts all values with `convertForArray` * (implemented in a subclass), and copies the @@ -198,15 +208,14 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A // @tailrec private final def lookupOrNull(hash: Long, shift: Int): V = { - this.getValueOrNodeOrNull(hash, shift) match { + this.getValueOrNodeOrNullOrTombstone(hash, shift) match { case null => nullOf[V] case node: Hamt[_, _, _, _, _, _] => node.lookupOrNull(hash, shift + W).asInstanceOf[V] case value => val a = value.asInstanceOf[V] - val hashA = a.key.hash - if (hash == hashA) { + if ((!a.isTomb) && (hash == a.key.hash)) { a } else { nullOf[V] @@ -214,7 +223,7 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A } } - private[this] final def getValueOrNodeOrNull(hash: Long, shift: Int): AnyRef = { + private[this] final def getValueOrNodeOrNullOrTombstone(hash: Long, shift: Int): AnyRef = { val bitmap = this.bitmap if (bitmap != 0L) { val flag: Long = 1L << logicalIdx(hash, shift) // only 1 bit set, at the position in bitmap @@ -233,16 +242,9 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A } private final def visit[T](k: K, hash: Long, tok: T, visitor: Hamt.EntryVisitor[K, V, T], modify: Boolean, shift: Int): H = { - this.getValueOrNodeOrNull(hash, shift) match { + this.getValueOrNodeOrNullOrTombstone(hash, shift) match { case null => - visitor.entryAbsent(k, tok) match { - case null => - nullOf[H] - case newVal => - _assert(newVal.key.hash == hash) - // TODO: this will compute physIdx again: - this.insertOrOverwrite(hash, newVal, shift, op = OP_INSERT) - } + this.visitEntryAbsent(k, hash, tok, visitor, shift = shift) case node: Hamt[_, _, _, _, _, _] => node.asInstanceOf[H].visit(k, hash, tok, visitor, modify = modify, shift = shift + W) match { case null => @@ -250,7 +252,7 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A case newNode => val oldSize = this.size val newSize = oldSize + (newNode.size - node.size) - _assert((modify && ((newSize == oldSize) || (newSize == (oldSize + 1)))) || (newSize == (oldSize + 1))) + _assert((modify && ((newSize >= (oldSize - 1)) && (newSize <= (oldSize + 1)))) || (newSize == (oldSize + 1))) val bitmap = this.bitmap // TODO: we're computing physIdx twice: val physIdx: Int = physicalIdx(bitmap, 1L << logicalIdx(hash, shift)) @@ -260,31 +262,39 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A val a = value.asInstanceOf[V] val hashA = a.key.hash if (hash == hashA) { - val newEntry = visitor.entryPresent(k, a, tok) - if (modify) { - if (equ(newEntry, a)) { - nullOf[H] + if (!a.isTomb) { + val newEntry = visitor.entryPresent(k, a, tok) + if (modify) { + if (equ(newEntry, a)) { + nullOf[H] + } else { + _assert(newEntry.key.hash == hashA) + this.insertOrOverwrite(hashA, newEntry, shift, op = OP_UPDATE) + } } else { - _assert(newEntry.key.hash == hashA) - this.insertOrOverwrite(hashA, newEntry, shift, op = OP_UPDATE) + _assert(equ(newEntry, a)) + nullOf[H] } } else { - _assert(equ(newEntry, a)) - nullOf[H] + this.visitEntryAbsent(k, hash, tok, visitor, shift = shift) } } else { - visitor.entryAbsent(k, tok) match { - case null => - nullOf[H] - case newVal => - _assert(newVal.key.hash == hash) - // TODO: this will compute physIdx again: - this.insertOrOverwrite(hash, newVal, shift, op = OP_INSERT) - } + this.visitEntryAbsent(k, hash, tok, visitor, shift = shift) } } } + private[this] final def visitEntryAbsent[T](k: K, hash: Long, tok: T, visitor: Hamt.EntryVisitor[K, V, T], shift: Int): H = { + visitor.entryAbsent(k, tok) match { + case null => + nullOf[H] + case newVal => + _assert(newVal.key.hash == hash) + // TODO: this will compute physIdx again: + this.insertOrOverwrite(hash, newVal, shift, op = OP_INSERT) + } + } + private final def insertOrOverwrite(hash: Long, value: V, shift: Int, op: Int): H = { val flag: Long = 1L << logicalIdx(hash, shift) // only 1 bit set, at the position in bitmap val bitmap = this.bitmap @@ -301,13 +311,22 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A case newNode => this.withNode(this.size + (newNode.size - node.size), bitmap, newNode, physIdx) } - case ov => - val oh = ov.asInstanceOf[V].key.hash + case ov => // or Tombstone + val ovv = ov.asInstanceOf[V] + val oh = ovv.key.hash if (hash == oh) { - if (op == OP_INSERT) { - throw new IllegalArgumentException - } else if (equ(ov, value)) { - nullOf[H] + if (ovv.isTomb) { + if (op == OP_UPDATE) { + throw new IllegalArgumentException + } + } else { + if (op == OP_INSERT) { + throw new IllegalArgumentException + } + } + // ok, checked for errors, now do the thing: + if (equ(ovv, value)) { + nullOf[H] // nothing to do } else { this.withValue(bitmap, value, physIdx) } @@ -316,9 +335,10 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A // so we go down one level: val childNode = { val cArr = new Array[AnyRef](1) - cArr(0) = ov + cArr(0) = ovv val oFlag = 1L << logicalIdx(oh, shift + W) - this.newNode(sizeAndBlue = packSizeAndBlueInternal(1, isBlue(ov.asInstanceOf[V])), bitmap = oFlag, contents = cArr) + val cSize = if (ovv.isTomb) 0 else 1 + this.newNode(sizeAndBlue = packSizeAndBlueInternal(cSize, isBlueOrTomb(ovv)), bitmap = oFlag, contents = cArr) } val childNode2 = childNode.insertOrOverwrite(hash, value, shift + W, op) this.withNode(this.size + (childNode2.size - 1), bitmap, childNode2, physIdx) @@ -333,8 +353,9 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A val len = contents.length val newArr = new Array[AnyRef](len + 1) System.arraycopy(contents, 0, newArr, 0, physIdx) - newArr(physIdx) = box(value) + newArr(physIdx) = value System.arraycopy(contents, physIdx, newArr, physIdx + 1, len - physIdx) + _assert(!value.isTomb) this.newNode( sizeAndBlue = packSizeAndBlueInternal(this.size + 1, this.isBlueSubtree && isBlue(value)), bitmap = newBitmap, @@ -348,7 +369,8 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A throw new IllegalArgumentException } else { val newArr = new Array[AnyRef](1) - newArr(0) = box(value) + newArr(0) = value + _assert(!value.isTomb) this.newNode(packSizeAndBlueInternal(1, isBlue(value)), flag, newArr) } } @@ -363,11 +385,16 @@ private[mcas] abstract class Hamt[K <: Hamt.HasHash, V <: Hamt.HasKey[K], E <: A } } + private[this] final def isBlueOrTomb(value: V): Boolean = { + value.isTomb || this.isBlue(value) + } + private[this] final def withValue(bitmap: Long, value: V, physIdx: Int): H = { + val sizeDiff = if (value.isTomb) -1 else 0 // NB: we never overwrite a tombstone with a tombstone this.newNode( - sizeAndBlue = packSizeAndBlueInternal(this.size, this.isBlueSubtree && isBlue(value)), + sizeAndBlue = packSizeAndBlueInternal(this.size + sizeDiff, this.isBlueSubtree && isBlueOrTomb(value)), bitmap = bitmap, - contents = arrReplacedValue(this.contents, box(value), physIdx), + contents = arrReplacedValue(this.contents, value, physIdx), ) } @@ -421,6 +448,7 @@ private[choam] object Hamt { trait HasKey[K <: HasHash] { def key: K + def isTomb: Boolean } trait HasHash { @@ -431,4 +459,15 @@ private[choam] object Hamt { def entryPresent(k: K, v: V, tok: T): V def entryAbsent(k: K, tok: T): V } + + final class Tombstone(final override val hash: Long) + extends HasKey[Tombstone] + with HasHash { + + final override def key: Tombstone = + this + + final override def isTomb: Boolean = + true + } } diff --git a/mcas/shared/src/test/scala/dev/tauri/choam/internal/mcas/HamtSpec.scala b/mcas/shared/src/test/scala/dev/tauri/choam/internal/mcas/HamtSpec.scala index 556ecc96..c26f8a63 100644 --- a/mcas/shared/src/test/scala/dev/tauri/choam/internal/mcas/HamtSpec.scala +++ b/mcas/shared/src/test/scala/dev/tauri/choam/internal/mcas/HamtSpec.scala @@ -670,6 +670,9 @@ object HamtSpec { final override val key: LongWr = LongWr(value) + final override def isTomb: Boolean = + false + override def equals(that: Any): Boolean = { if (that.isInstanceOf[SpecVal]) { that.equals(this)