diff --git a/examples/stdlib/acme.effekt b/examples/stdlib/acme.effekt index b8e209be7..112f36adf 100644 --- a/examples/stdlib/acme.effekt +++ b/examples/stdlib/acme.effekt @@ -11,6 +11,7 @@ import char import dequeue import effekt import exception +import heap import io import io/console import io/error @@ -25,6 +26,7 @@ import process import queue import ref import regex +import resizable_array import result import scanner import seq @@ -33,5 +35,6 @@ import stream import string import test import tty +import union_find def main() = () diff --git a/examples/stdlib/heap/heap.check b/examples/stdlib/heap/heap.check new file mode 100644 index 000000000..23c6f2f4c --- /dev/null +++ b/examples/stdlib/heap/heap.check @@ -0,0 +1,6 @@ +HeapTests +✓ simple heap sort on integers + + 1 pass + 0 fail + 1 tests total \ No newline at end of file diff --git a/examples/stdlib/heap/heap.effekt b/examples/stdlib/heap/heap.effekt new file mode 100644 index 000000000..e43e1cc31 --- /dev/null +++ b/examples/stdlib/heap/heap.effekt @@ -0,0 +1,27 @@ +import heap +import test + +def main() = { + suite("HeapTests", false) { + test("simple heap sort on integers") { + with on[OutOfBounds].default { assertTrue(false); <> }; + val h = heap[Int](box { (x: Int, y: Int) => + if (x < y) { + Less() + } else Greater() // hacky, should sometimes be Equal() + }) + h.insert(12) + h.insert(10) + h.insert(7) + h.insert(11) + h.insert(14) + assert(h.deleteMin(), 7) + assert(h.deleteMin(), 10) + assert(h.deleteMin(), 11) + assert(h.deleteMin(), 12) + assert(h.deleteMin(), 14) + assert(h.size, 0) + } + }; + () +} \ No newline at end of file diff --git a/examples/stdlib/resizable_array/resizable_array.check b/examples/stdlib/resizable_array/resizable_array.check new file mode 100644 index 000000000..d99b8f8f4 --- /dev/null +++ b/examples/stdlib/resizable_array/resizable_array.check @@ -0,0 +1,6 @@ +ResizableArrayTests +✓ usage as stack + + 1 pass + 0 fail + 1 tests total \ No newline at end of file diff --git a/examples/stdlib/resizable_array/resizable_array.effekt b/examples/stdlib/resizable_array/resizable_array.effekt new file mode 100644 index 000000000..d029535d1 --- /dev/null +++ b/examples/stdlib/resizable_array/resizable_array.effekt @@ -0,0 +1,32 @@ +import resizable_array +import test + +def main() = { + suite("ResizableArrayTests", false) { + test("usage as stack") { + with on[OutOfBounds].default { assertTrue(false, "out of bounds") } + val a = resizableArray() + a.add(1) + a.add(1) + a.add(2) + a.add(3) + a.add(13) + a.add(21) + a.add(34) + a.add(55) + assert(a.popRight(), 55) + assert(a.popRight(), 34) + assert(a.popRight(), 21) + assert(a.popRight(), 13) + a.add(5) + a.add(8) + assert(a.popRight(), 8) + assert(a.popRight(), 5) + assert(a.popRight(), 3) + assert(a.popRight(), 2) + assert(a.popRight(), 1) + assert(a.popRight(), 1) + } + }; + () +} \ No newline at end of file diff --git a/examples/stdlib/union_find/union_find.check b/examples/stdlib/union_find/union_find.check new file mode 100644 index 000000000..ea6f0c408 --- /dev/null +++ b/examples/stdlib/union_find/union_find.check @@ -0,0 +1,6 @@ +UnionFindTests +✓ three elements + + 1 pass + 0 fail + 1 tests total \ No newline at end of file diff --git a/examples/stdlib/union_find/union_find.effekt b/examples/stdlib/union_find/union_find.effekt new file mode 100644 index 000000000..0acd00625 --- /dev/null +++ b/examples/stdlib/union_find/union_find.effekt @@ -0,0 +1,21 @@ +import union_find +import test + +def main() = { + suite("UnionFindTests", false) { + test("three elements") { + with on[MissingValue].default { assertTrue(false, "MissingValue exception") } + val u = unionFind() + val a = u.makeSet() + val b = u.makeSet() + val c = u.makeSet() + assert(u.find(a), a) + assert(u.find(b), b) + assert(u.find(c), c) + u.union(a,b) + assert(u.find(a), u.find(b)) + assert(u.find(c), c) + } + }; + () +} \ No newline at end of file diff --git a/libraries/common/heap.effekt b/libraries/common/heap.effekt new file mode 100644 index 000000000..df1c6ed5c --- /dev/null +++ b/libraries/common/heap.effekt @@ -0,0 +1,103 @@ +module heap +import resizable_array + +/// Resizable 2-ary min-heap, backed by a resizable array +/// `cmp` defines the ordering of elements +record Heap[T](rawContents: ResizableArray[T], cmp: (T, T) => Ordering at {}) + +/// Make a new Heap with the given comparison operation +def heap[T](cmp: (T,T) => Ordering at {}) = + Heap[T](resizableArray(), cmp) + +/// Make a new Heap with the given comparison operation and initial capacity +def heap[T](cmp: (T,T) => Ordering at {}, capacity: Int) = + Heap[T](resizableArray(capacity), cmp) + +namespace internal { + def left(idx: Int) = 2 * idx + 1 + def right(idx: Int) = 2 * idx + 2 + def parent(idx: Int) = (idx - 1) / 2 + + def bubbleUp[A](heap: Heap[A], idx: Int) = { + val arr = heap.rawContents + arr.boundsCheck(idx) // idx > parent(idx), parent(parent(idx)) etc + + def go(idx: Int): Unit = { + if (idx > 0 and (heap.cmp)(arr.unsafeGet(parent(idx)), arr.unsafeGet(idx)) is Greater()) { + arr.unsafeSwap(parent(idx), idx) + go(parent(idx)) + } + } + go(idx) + } + def sinkDown[A](heap: Heap[A], idx: Int) = { + val arr = heap.rawContents + + def infixLt(x: A, y: A) = { + if ((heap.cmp)(x,y) is Less()) { true } else { false } + } + + def go(idx: Int): Unit = { + if (right(idx) < arr.size) { + val v = arr.unsafeGet(idx) + val l = arr.unsafeGet(left(idx)) + val r = arr.unsafeGet(right(idx)) + if (l < v && r < v) { + // swap with the smaller one + if (l < r) { + arr.unsafeSwap(left(idx), idx) + go(left(idx)) + } else { + arr.unsafeSwap(right(idx), idx) + go(right(idx)) + } + } else if (l < v) { + arr.unsafeSwap(left(idx), idx) + go(left(idx)) + } else if (r < v) { + arr.unsafeSwap(right(idx), idx) + go(right(idx)) + } + } else if (left(idx) < arr.size) { + if (arr.unsafeGet(left(idx)) < arr.unsafeGet(idx)) { + arr.unsafeSwap(left(idx), idx) + go(left(idx)) + } + } // else: we are at the bottom + } + go(idx) + } +} + +/// Insert value into heap +/// +/// O(log n) worst case if capacity suffices, O(1) average +def insert[T](heap: Heap[T], value: T): Unit = { + with on[OutOfBounds].panic(); + val idx = heap.rawContents.add(value) + internal::bubbleUp(heap, idx) +} + +/// find and return (but not remove) the minimal element in this heap +/// +/// O(1) +def findMin[T](heap: Heap[T]): T / Exception[OutOfBounds] = { + heap.rawContents.get(0) +} + +/// find and remove the minimal element in this heap +/// +/// O(log n) +def deleteMin[T](heap: Heap[T]): T / Exception[OutOfBounds] = { + val res = heap.rawContents.get(0) + heap.rawContents.unsafeSet(0, heap.rawContents.popRight()) + internal::sinkDown(heap, 0) + res +} + +/// Number of elements in the heap +/// +/// O(1) +def size[T](heap: Heap[T]): Int = { + heap.rawContents.size +} \ No newline at end of file diff --git a/libraries/common/resizable_array.effekt b/libraries/common/resizable_array.effekt new file mode 100644 index 000000000..bcd4ee479 --- /dev/null +++ b/libraries/common/resizable_array.effekt @@ -0,0 +1,235 @@ +module resizable_array + +import ref +import array + +record ResizableArray[T](rawSizePtr: Ref[Int], rawContentPtr: Ref[Array[T]]) + +// These numbers should be optimized based on benchmarking +// According to https://en.wikipedia.org/wiki/Dynamic_array most use 1.5 or 2 + +/// Factor by which to grow the capacity when it becomes too small +val growFactor = 1.5 +/// shrink array when size / capacity falls below this threshold +/// should be < 1/growFactor +val shrinkThreshold = 0.4 + +/// Number of elements in the dynamic array +/// +/// O(1) +def size[T](arr: ResizableArray[T]) = arr.rawSizePtr.get + +/// Allocate a new, empty dynamic array with given initial capacity +def resizableArray[T](capacity: Int): ResizableArray[T] = { + ResizableArray(ref(0), ref(allocate(capacity))) +} + +/// Allocate a new, empty dynamic array +def resizableArray[T](): ResizableArray[T] = resizableArray(8) + +/// Throw an OutOfBounds exception if index is not a valid index into arr +def boundsCheck[T](arr: ResizableArray[T], index: Int): Unit / Exception[OutOfBounds] = { + if (index < 0 && index >= arr.size) { + do raise(OutOfBounds(), "Array index out of bounds: " ++ show(index)) + } +} + +/// get the element at position index in the array +/// +/// precondition: index is a valid index into the array +/// +/// O(1) +def unsafeGet[T](arr: ResizableArray[T], index: Int): T = { + arr.rawContentPtr.get.unsafeGet(index) +} +/// get the element at position index in the array +/// +/// O(1) +def get[T](arr: ResizableArray[T], index: Int): T / Exception[OutOfBounds] = { + arr.boundsCheck(index); + arr.unsafeGet(index) +} + +/// set the element at position index in the array +/// +/// precondition: index is a valid index into the array +/// +/// O(1) +def unsafeSet[T](arr: ResizableArray[T], index: Int, value: T): Unit = { + arr.rawContentPtr.get.unsafeSet(index, value) +} + +/// set the element at position index in the array +/// +/// O(1) +def set[T](arr: ResizableArray[T], index: Int, value: T): Unit / Exception[OutOfBounds] = { + arr.boundsCheck(index); + arr.unsafeSet(index, value) +} + +/// swap the elements at the given positions in the array +/// +/// precondition: both are valid indices into the array +/// +/// O(1) +def unsafeSwap[T](arr: ResizableArray[T], index1: Int, index2: Int): Unit = { + val raw = arr.rawContentPtr.get + val tmp = raw.unsafeGet(index1) + raw.unsafeSet(index1, raw.unsafeGet(index2)) + raw.unsafeSet(index2, tmp) +} + +/// swap the elements at the given positions in the array +/// +/// O(1) +def swap[T](arr: ResizableArray[T], index1: Int, index2: Int): Unit / Exception[OutOfBounds] = { + arr.boundsCheck(max(index1, index2)) + arr.unsafeSwap(index1, index2) +} + +/// Change the dynamic to have exactly the given capacity +/// +/// precondition: given capacity is at least the size of the array +/// +/// O(n) +def unsafeSetCapacity[T](arr: ResizableArray[T], capacity: Int): Unit = { + with on[OutOfBounds].panic + val oldRaw = arr.rawContentPtr.get + if (oldRaw.size != capacity) { + val newRaw = array::allocate(capacity) + oldRaw.copy(0, newRaw, 0, arr.size) + arr.rawContentPtr.set(newRaw) + } +} + +/// Change the resizable array to have exactly the given capacity. +/// This only changes the size of the backing array, not the `size`. +/// +/// O(n) +def setCapacity[T](arr: ResizableArray[T], capacity: Int): Unit / Exception[OutOfBounds] = { + if (arr.size > capacity) { + do raise(OutOfBounds(), "Cannot change capacity of ResizableArray to " ++ capacity.show ++ " below size " ++ arr.size.show) + } + arr.unsafeSetCapacity(capacity) +} + +/// If the shrinkThreshold is reached, shrink by growFactor, otherwise do nothing +/// +/// O(n) +def maybeShrink[T](arr: ResizableArray[T]): Unit = { + if(arr.size.toDouble < arr.rawContentPtr.get.size.toDouble * shrinkThreshold) { + val newCap = max(arr.size, (arr.rawContentPtr.get.size.toDouble / growFactor).ceil) + arr.unsafeSetCapacity(newCap) + } +} + +/// makes sure capacity is at least the given one +/// +/// O(given capacity - current capacity) amortized, O(n) worst case // TODO ? +def ensureCapacity[T](arr: ResizableArray[T], capacity: Int): Unit / Exception[OutOfBounds] = { + if (arr.rawContentPtr.get.size < capacity) { + val curCapd: Double = arr.rawContentPtr.get.size.toDouble + val minGrowCapacity: Int = (curCapd * growFactor + 1.0).toInt + val newCapacity = max(capacity, minGrowCapacity) + arr.setCapacity(newCapacity) + } +} + +/// Set the value at given position, resizing if necessary +/// Note: New elements might be uninitialized!!! +/// +/// O(max(1,index - n)) amortized, O(n) worst case if index > capacity +def setResizing[T](arr: ResizableArray[T], index: Int, value: T): Unit / Exception[OutOfBounds] = { + if (index < 0) { + do raise(OutOfBounds(), "Negative index " ++ index.show) + } + if (index < arr.size) { + arr.rawContentPtr.get.unsafeSet(index, value) + } + ensureCapacity(arr, index + 1) + arr.rawContentPtr.get.unsafeSet(index, value) + arr.rawSizePtr.set(index + 1) +} + +/// Add a new element at the end of the resizable array. +/// Return the index of the new element +/// +/// O(1) amortized, O(n) worst case +def add[T](arr: ResizableArray[T], value: T): Int = { + with on[OutOfBounds].panic(); + val idx = arr.size + arr.setResizing(idx, value) + idx +} + +/// Remove and return the rightmost element in the resizable array. +/// +/// O(1) amortized, O(n) worst case +def popRight[T](arr: ResizableArray[T]): T / Exception[OutOfBounds] = { + arr.boundsCheck(arr.size - 1) + arr.rawSizePtr.set(arr.size - 1) + val r = arr.unsafeGet(arr.size) + arr.maybeShrink() + r +} + + +def foreachIndex[T](arr: ResizableArray[T]){ body: (Int, T) => Unit }: Unit = { + each(0, arr.size) { i => + body(i, arr.rawContentPtr.get.unsafeGet(i)) + } +} +def foreachIndex[T](arr: ResizableArray[T]){ body: (Int, T) {Control} => Unit }: Unit = { + each(0, arr.size) { (i){label} => + body(i, arr.rawContentPtr.get.unsafeGet(i)){label} + } +} +def foreach[T](arr: ResizableArray[T]){ body: T => Unit }: Unit = { + each(0, arr.size) { i => + body(arr.rawContentPtr.get.unsafeGet(i)) + } +} +def foreach[T](arr: ResizableArray[T]){ body: (T) {Control} => Unit }: Unit = { + each(0, arr.size) { (i){label} => + body(arr.rawContentPtr.get.unsafeGet(i)){label} + } +} +def foreachIndexReversed[T](arr: ResizableArray[T]){ body: (Int, T){Control} => Unit }: Unit = { + var i = arr.size - 1 + loop { {l} => + if (i < 0) { l.break() } + body(i, arr.rawContentPtr.get.unsafeGet(i)){l} + i = i - 1 + } +} +def foreachIndexReversed[T](arr: ResizableArray[T]){ body: (Int, T) => Unit }: Unit = { + var i = arr.size - 1 + while(i >= 0) { + body(i, arr.rawContentPtr.get.unsafeGet(i)) + i = i - 1 + } +} +def foreachReversed[T](arr: ResizableArray[T]){ body: (T){Control} => Unit }: Unit = { + var i = arr.size - 1 + loop { {l} => + if (i < 0) { l.break() } + body(arr.rawContentPtr.get.unsafeGet(i)){l} + i = i - 1 + } +} +def foreachReversed[T](arr: ResizableArray[T]){ body: T => Unit }: Unit = { + var i = arr.size - 1 + while (i >= 0) { + body(arr.rawContentPtr.get.unsafeGet(i)) + i = i - 1 + } +} + +def toList[T](arr: ResizableArray[T]): List[T] = { + def go(i: Int, acc: List[T]): List[T] = { + if (i < 0) { acc } else { + go(i - 1, Cons(arr.unsafeGet(i), acc)) + } + } + go(arr.size - 1, Nil()) +} \ No newline at end of file diff --git a/libraries/common/union_find.effekt b/libraries/common/union_find.effekt new file mode 100644 index 000000000..b11d92552 --- /dev/null +++ b/libraries/common/union_find.effekt @@ -0,0 +1,113 @@ +module union_find +import resizable_array + +/// Classic mutable union-find data structure on integer indices, +/// with union-by-rank and path compression. +record UnionFind(rawElements: ResizableArray[Int]) + +namespace internal { + // -(rk + 1) for roots + // parent for inner nodes + def isRoot(u: UnionFind, s: Int): Bool / Exception[MissingValue] = { + with on[OutOfBounds].default { do raise(MissingValue(), "No node " ++ s.show) } + u.rawElements.get(s) < 0 + } + def parent(u: UnionFind, s: Int): Int / Exception[MissingValue] = { + with on[OutOfBounds].default { do raise(MissingValue(), "No node " ++ s.show) } + val c = u.rawElements.get(s) + if (c < 0) { + do raise(MissingValue(), "Root node " ++ s.show ++ " has no parent.") + } else { + c + } + } + def rank(u: UnionFind, s: Int): Int / Exception[MissingValue] = { + with on[OutOfBounds].default { do raise(MissingValue(), "No node " ++ s.show) } + val c = u.rawElements.get(s) + if (c < 0) { + -1 * c - 1 + } else { + do raise(MissingValue(), "Only root nodes have a rank.") + } + } + def setParent(u: UnionFind, child: Int, parent: Int) = { + with on[OutOfBounds].default { do raise(MissingValue(), "No node " ++ child.show) } + u.rawElements.set(child, parent) + } + def setRank(u: UnionFind, root: Int, rank: Int) = { + with on[OutOfBounds].default { do raise(MissingValue(), "No node " ++ root.show) } + u.rawElements.set(root, -1 * rank - 1) + } +} + +def unionFind(): UnionFind = + UnionFind(resizableArray()) + +def unionFind(capacity: Int): UnionFind = + UnionFind(resizableArray(capacity)) + +/// Make a new set in the union-find datastructure and return its index +/// +/// O(1) amortized, worst-case O(n) if the capacity does not suffice +def makeSet(u: UnionFind): Int = { + u.rawElements.add(-1) +} + +/// Find the representative of the given element. +/// +/// O(inverse-ackermann(n)) amortized +def find(u: UnionFind, s: Int): Int / Exception[MissingValue] = { + def goFind(s: Int): Int = { + if (u.internal::isRoot(s)) { + s + } else { + goFind(u.internal::parent(s)) + } + } + val root = goFind(s) + + def goCompress(s: Int): Unit = { + if (not(u.internal::isRoot(s))) { + val p = u.internal::parent(s) + u.internal::setParent(s, root) + goCompress(p) + } + } + goCompress(s) + + root +} + +/// Make it such that the sets *represented by* x and y are merged +/// +/// O(1) +def unionRoots(u: UnionFind, x: Int, y: Int): Int / Exception[MissingValue] = { + if(not(u.internal::isRoot(x) && u.internal::isRoot(y))) { + panic("unionRoots should only be called on representatives") + } + + def addChild(root: Int, child: Int) = { + if (u.internal::rank(root) == u.internal::rank(child)) { + u.internal::setRank(root, u.internal::rank(root) + 1) + } + u.internal::setParent(child, root) + root + } + + if (x == y) { + x + } else if (u.internal::rank(x) < u.internal::rank(y)) { + addChild(y, x) + } else { + addChild(x, y) + } +} + +/// Make it so a and b are in the same set +/// +/// O(inverse-ackermann(n)) amortized +def union(u: UnionFind, a: Int, b: Int): Int / Exception[MissingValue] = { + val x = u.find(a) + val y = u.find(b) + u.unionRoots(x, y) +} \ No newline at end of file