diff --git a/Changelog.md b/Changelog.md index 4125e853d..66e5f16d7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,9 @@ ## Next +* Add `reverse` function to the `pure/Queue` module (#229). +* Add `pure/RealTimeQueue` module - an alternative immutable double-ended queue implementation with worst-case `O(1)` time complexity for all operations but worse amortized performance (#229). + - Refer to the `Queues.bench.mo` benchmark for comparison with other queue implementations. * Rename `Map.replaceIfExists()` to `Map.replace()` (#286). * Add `entriesFrom` and `reverseEntriesFrom` to `Map`, `valuesFrom` and `reverseValuesFrom` to `Set` and `Text.toText` (#272). * Update code examples in doc comments (#224, #282). diff --git a/bench/Queues.bench.mo b/bench/Queues.bench.mo new file mode 100644 index 000000000..233efea50 --- /dev/null +++ b/bench/Queues.bench.mo @@ -0,0 +1,108 @@ +import Bench "mo:bench"; + +import Array "../src/Array"; +import Nat "../src/Nat"; +import Option "../src/Option"; +import OldQueue "../src/pure/Queue"; +import NewQueue "../src/pure/RealTimeQueue"; +import MutQueue "../src/Queue"; +import Runtime "../src/Runtime"; + +module { + public func init() : Bench.Bench { + let bench = Bench.Bench(); + + bench.name("Different queue implementations"); + bench.description("Compare the performance of the following queue implementations_: +- `pure/Queue`: The default immutable double-ended queue implementation. + * Pros: Good amortized performance, meaning that the average cost of operations is low `O(1)`. + * Cons: In worst case, an operation can take `O(size)` time rebuilding the queue as demonstrated in the `Pop front 2 elements` scenario. +- `pure/RealTimeQueue` + * Pros: Every operation is guaranteed to take at most `O(1)` time and space. + * Cons: Poor amortized performance: Instruction cost is on average 4x for *pop* and 10x for *push* compared to `pure/Queue`. +- mutable `Queue` + * Pros: Also `O(1)` guarantees with a lower constant factor than `pure/RealTimeQueue`. Amortized performance is comparable to `pure/Queue`. + * Cons: It is mutable and cannot be used in `shared` types (not shareable)_."); + + bench.rows([ + "Initialize with 2 elements", + "Push 500 elements", + "Pop front 2 elements", + "Pop 150 front&back" + ]); + bench.cols([ + "pure/Queue", + "pure/RealTimeQueue", + "mutable Queue" + ]); + + let init = Array.repeat(1, 2); + var newQ = NewQueue.empty(); + var oldQ = OldQueue.empty(); + var mutQ = MutQueue.empty(); + + let toPush = Array.repeat(7, 500); + + bench.runner( + func(row, col) = switch (col, row) { + case ("pure/RealTimeQueue", "Initialize with 2 elements") newQ := NewQueue.fromIter(init.vals()); + case ("pure/Queue", "Initialize with 2 elements") oldQ := OldQueue.fromIter(init.vals()); + case ("mutable Queue", "Initialize with 2 elements") mutQ := MutQueue.fromIter(init.vals()); + case ("pure/RealTimeQueue", "Push 500 elements") { + for (i in toPush.vals()) { + newQ := NewQueue.pushBack(newQ, i) + } + }; + case ("pure/Queue", "Push 500 elements") { + for (i in toPush.vals()) { + oldQ := OldQueue.pushBack(oldQ, i) + } + }; + case ("mutable Queue", "Push 500 elements") { + for (i in toPush.vals()) { + MutQueue.pushBack(mutQ, i) + } + }; + case ("pure/RealTimeQueue", "Pop front 2 elements") Option.unwrap( + do ? { + newQ := NewQueue.popFront(NewQueue.popFront(newQ)!.1)!.1 + } + ); + case ("pure/Queue", "Pop front 2 elements") Option.unwrap( + do ? { + oldQ := OldQueue.popFront(OldQueue.popFront(oldQ)!.1)!.1 + } + ); + case ("mutable Queue", "Pop front 2 elements") Option.unwrap( + do ? { + ignore MutQueue.popFront(mutQ); + ignore MutQueue.popFront(mutQ) + } + ); + case ("pure/RealTimeQueue", "Pop 150 front&back") { + for (i in Nat.range(0, 150)) Option.unwrap( + do ? { + newQ := NewQueue.popBack(NewQueue.popFront(newQ)!.1)!.0 + } + ) + }; + case ("pure/Queue", "Pop 150 front&back") { + for (i in Nat.range(0, 150)) Option.unwrap( + do ? { + oldQ := OldQueue.popBack(OldQueue.popFront(oldQ)!.1)!.0 + } + ) + }; + case ("mutable Queue", "Pop 150 front&back") { + for (i in Nat.range(0, 150)) { + ignore MutQueue.popFront(mutQ); + ignore MutQueue.popBack(mutQ) + } + }; + case _ Runtime.unreachable() + } + ); + + bench + } +} diff --git a/log b/log new file mode 100644 index 000000000..80aeda51e --- /dev/null +++ b/log @@ -0,0 +1,63 @@ + +> @dfinity/new-motoko-base@2.0.0 bench +> mops bench Queues + +Benchmark files: +• bench/Queues.bench.mo + +================================================== + +Starting dfx replica... +Deploying canisters... + +-------------------------------------------------- + +Running bench/Queues.bench.mo... + + + +Different queue implementations + +Compare the performance of the following queue implementations_: +- `pure/Queue`: The default immutable double-ended queue implementation. + * Pros: Good amortized performance, meaning that the average cost of operations is low `O(1)`. + * Cons: In worst case, an operation can take `O(size)` time rebuilding the queue as demonstrated in the `Pop front 2 elements` scenario. +- `pure/RealTimeQueue` + * Pros: Every operation is guaranteed to take at most `O(1)` time and space. + * Cons: Poor amortized performance: Instruction cost is on average 4x for *pop* and 10x for *push* compared to `pure/Queue`. +- mutable `Queue` + * Pros: Also `O(1)` guarantees with a lower constant factor than `pure/RealTimeQueue`. Amortized performance is comparable to `pure/Queue`. + * Cons: It is mutable and cannot be used in `shared` types (not shareable)_. + + +Instructions + +| | pure/Queue | pure/RealTimeQueue | mutable Queue | +| :------------------------- | ---------: | -----------------: | ------------: | +| Initialize with 2 elements | 3_571 | 2_934 | 3_401 | +| Push 500 elements | 103_492 | 966_398 | 243_585 | +| Pop front 2 elements | 98_792 | 5_009 | 4_326 | +| Pop 150 front&back | 106_545 | 394_667 | 140_211 | + + +Heap + +| | pure/Queue | pure/RealTimeQueue | mutable Queue | +| :------------------------- | ---------: | -----------------: | ------------: | +| Initialize with 2 elements | 324 B | 300 B | 352 B | +| Push 500 elements | 8.08 KiB | 8.17 KiB | 19.8 KiB | +| Pop front 2 elements | 240 B | 240 B | 192 B | +| Pop 150 front&back | -4.42 KiB | -492 B | -11.45 KiB | + + +Garbage Collection + +| | pure/Queue | pure/RealTimeQueue | mutable Queue | +| :------------------------- | ---------: | -----------------: | ------------: | +| Initialize with 2 elements | 508 B | 472 B | 456 B | +| Push 500 elements | 10.1 KiB | 166.63 KiB | 344 B | +| Pop front 2 elements | 12.19 KiB | 528 B | 424 B | +| Pop 150 front&back | 15.61 KiB | 60.38 KiB | 12.1 KiB | + + +Stopping replica... diff --git a/src/pure/Queue.mo b/src/pure/Queue.mo index 39d9cf8b4..fff296386 100644 --- a/src/pure/Queue.mo +++ b/src/pure/Queue.mo @@ -20,6 +20,10 @@ /// /// `n` denotes the number of elements stored in the queue. /// +/// Note that some operations that traverse the elements of the queue (e.g. `forEach`, `values`) preserve the order of the elements, +/// whereas others (e.g. `map`, `contains`) do NOT guarantee that the elements are visited in any order. +/// The order is undefined to avoid allocations, making these operations more efficient. +/// /// ```motoko name=import /// import Queue "mo:base/pure/Queue"; /// ``` @@ -115,9 +119,9 @@ module { /// } /// ``` /// - /// Runtime: O(size) + /// Runtime: `O(size)` /// - /// Space: O(1) + /// Space: `O(1)` public func contains(queue : Queue, equal : (T, T) -> Bool, item : T) : Bool = List.contains(queue.0, equal, item) or List.contains(queue.2, equal, item); /// Inspect the optional element on the front end of a queue. @@ -370,11 +374,11 @@ module { /// } /// ``` /// - /// Runtime: O(size) + /// Runtime: `O(size)` /// - /// Space: O(size) as the current implementation uses `values` to iterate over the queue. + /// Space: `O(size)` as the current implementation uses `values` to iterate over the queue. /// - /// *Runtime and space assumes that `f` runs in O(1) time and space. + /// *Runtime and space assumes that the `predicate` runs in `O(1)` time and space. public func all(queue : Queue, predicate : T -> Bool) : Bool { for (item in values queue) if (not (predicate item)) return false; return true @@ -392,11 +396,11 @@ module { /// } /// ``` /// - /// Runtime: O(size) + /// Runtime: `O(size)` /// - /// Space: O(size) as the current implementation uses `values` to iterate over the queue. + /// Space: `O(size)` as the current implementation uses `values` to iterate over the queue. /// - /// *Runtime and space assumes that `f` runs in O(1) time and space. + /// *Runtime and space assumes that the `predicate` runs in `O(1)` time and space. public func any(queue : Queue, predicate : T -> Bool) : Bool { for (item in values queue) if (predicate item) return true; return false @@ -415,17 +419,18 @@ module { /// } /// ``` /// - /// Runtime: O(size) + /// Runtime: `O(size)` /// - /// Space: O(size) + /// Space: `O(size)` /// - /// *Runtime and space assumes that `f` runs in O(1) time and space. + /// *Runtime and space assumes that `f` runs in `O(1)` time and space. public func forEach(queue : Queue, f : T -> ()) = for (item in values queue) f item; /// Call the given function `f` on each queue element and collect the results /// in a new queue. /// /// Note: The order of visiting elements is undefined with the current implementation. + /// /// Example: /// ```motoko include=import /// import Iter "mo:base/Iter"; @@ -438,10 +443,11 @@ module { /// } /// ``` /// - /// Runtime: O(size) + /// Runtime: `O(size)` /// - /// Space: O(size) - /// *Runtime and space assumes that `f` runs in O(1) time and space. + /// Space: `O(size)` + /// + /// *Runtime and space assumes that `f` runs in `O(1)` time and space. public func map(queue : Queue, f : T1 -> T2) : Queue { let (fr, n, b) = queue; (List.map(fr, f), n, List.map(b, f)) @@ -461,13 +467,15 @@ module { /// } /// ``` /// - /// Runtime: O(size) + /// Runtime: `O(size)` /// - /// Space: O(size) - public func filter(queue : Queue, f : T -> Bool) : Queue { + /// Space: `O(size)` + /// + /// *Runtime and space assumes that `predicate` runs in `O(1)` time and space. + public func filter(queue : Queue, predicate : T -> Bool) : Queue { let (fr, _, b) = queue; - let front = List.filter(fr, f); - let back = List.filter(b, f); + let front = List.filter(fr, predicate); + let back = List.filter(b, predicate); check(front, List.size front + List.size back, back) }; @@ -488,11 +496,11 @@ module { /// } /// ``` /// - /// Runtime: O(size) + /// Runtime: `O(size)` /// - /// Space: O(size) + /// Space: `O(size)` /// - /// *Runtime and space assumes that `f` runs in O(1) time and space. + /// *Runtime and space assumes that `f` runs in `O(1)` time and space. public func filterMap(queue : Queue, f : T -> ?U) : Queue { let (fr, _n, b) = queue; let front = List.filterMap(fr, f); @@ -513,9 +521,9 @@ module { /// } /// ``` /// - /// Runtime: O(size) + /// Runtime: `O(size)` /// - /// Space: O(size) + /// Space: `O(size)` public func toText(queue : Queue, f : T -> Text) : Text { var text = "PureQueue["; func add(item : T) { @@ -540,11 +548,11 @@ module { /// } /// ``` /// - /// Runtime: O(size) + /// Runtime: `O(size)` /// - /// Space: O(size) + /// Space: `O(size)` /// - /// *Runtime and space assumes that argument `compare` runs in O(1) time and space. + /// *Runtime and space assumes that argument `compareItem` runs in `O(1)` time and space. public func compare(queue1 : Queue, queue2 : Queue, compareItem : (T, T) -> Order.Order) : Order.Order { let (i1, i2) = (values queue1, values queue2); loop switch (i1.next(), i2.next()) { @@ -556,6 +564,23 @@ module { case (null, _) return #less; case (_, null) return #greater } - } + }; + /// Reverse the order of elements in a queue. + /// This operation is cheap, it does NOT require copying the elements. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.fromIter([1, 2, 3].values()); + /// let reversed = Queue.reverse(queue); + /// assert Queue.peekFront(reversed) == ?3; + /// assert Queue.peekBack(reversed) == ?1; + /// } + /// ``` + /// + /// Runtime: `O(1)` + /// + /// Space: `O(1)` + public func reverse(queue : Queue) : Queue = (queue.2, queue.1, queue.0) } diff --git a/src/pure/RealTimeQueue.mo b/src/pure/RealTimeQueue.mo new file mode 100644 index 000000000..7b0e60079 --- /dev/null +++ b/src/pure/RealTimeQueue.mo @@ -0,0 +1,1030 @@ +/// Double-ended immutable queue with guaranteed `O(1)` push/pop operations (caveat: high constant factor). +/// For a default immutable queue implementation, see `pure/Queue`. +/// +/// This module provides an alternative implementation with better worst-case performance for single operations, e.g. `pushBack` and `popFront`. +/// These operations are always constant time, `O(1)`, which eliminates spikes in performance of `pure/Queue` operations +/// that are caused by the amortized nature of the `pure/Queue` implementation, which can lead to `O(n)` worst-case performance for a single operation. +/// The spikes in performance can cause a single message to take multiple more rounds to complete than most other messages. +/// +/// However, the `O(1)` operations come at a cost of higher constant factor than the `pure/Queue` implementation: +/// - 'pop' operations are on average 4x more expensive +/// - 'push' operations are on average 10x more expensive +/// +/// For better performance across multiple operations and when the spikes in single operations are not a problem, use `pure/Queue`. +/// For guaranteed `O(1)` operations, use `pure/RealTimeQueue`. +/// +/// --- +/// +/// The interface is purely functional, not imperative, and queues are immutable values. +/// In particular, Queue operations such as push and pop do not update their input queue but, instead, return the +/// value of the modified Queue, alongside any other data. +/// The input queue is left unchanged. +/// +/// Examples of use-cases: +/// - Queue (FIFO) by using `pushBack()` and `popFront()`. +/// - Stack (LIFO) by using `pushFront()` and `popFront()`. +/// - Deque (double-ended queue) by using any combination of push/pop operations on either end. +/// +/// A Queue is internally implemented as a real-time double-ended queue based on the paper +/// "Real-Time Double-Ended Queue Verified (Proof Pearl)". The implementation maintains +/// worst-case constant time `O(1)` for push/pop operations through gradual rebalancing steps. +/// +/// Construction: Create a new queue with the `empty()` function. +/// +/// Note that some operations that traverse the elements of the queue (e.g. `forEach`, `values`) preserve the order of the elements, +/// whereas others (e.g. `map`, `contains`) do NOT guarantee that the elements are visited in any order. +/// The order is undefined to avoid allocations, making these operations more efficient. +/// +/// ```motoko name=import +/// import Queue "mo:base/pure/RealTimeQueue"; +/// ``` + +import Types "../Types"; +import List "List"; +import Option "../Option"; +import { trap } "../Runtime"; +import Iter "../Iter"; + +module { + /// The real-time queue data structure can be in one of the following states: + /// + /// - `#empty`: the queue is empty + /// - `#one`: the queue contains a single element + /// - `#two`: the queue contains two elements + /// - `#three`: the queue contains three elements + /// - `#idles`: the queue is in the idle state, where `l` and `r` are non-empty stacks of elements fulfilling the size invariant + /// - `#rebal`: the queue is in the rebalancing state + public type Queue = { + #empty; + #one : T; + #two : (T, T); + #three : (T, T, T); + #idles : (Idle, Idle); + #rebal : States + }; + + /// Create a new empty queue. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.empty(); + /// assert Queue.isEmpty(queue); + /// } + /// ``` + /// + /// Runtime: `O(1)`. + /// + /// Space: `O(1)`. + public func empty() : Queue = #empty; + + /// Determine whether a queue is empty. + /// Returns true if `queue` is empty, otherwise `false`. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.empty(); + /// assert Queue.isEmpty(queue); + /// } + /// ``` + /// + /// Runtime: `O(1)`. + /// + /// Space: `O(1)`. + public func isEmpty(queue : Queue) : Bool = switch queue { + case (#empty) true; + case _ false + }; + + /// Create a new queue comprising a single element. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.singleton(25); + /// assert Queue.size(queue) == 1; + /// assert Queue.peekFront(queue) == ?25; + /// } + /// ``` + /// + /// Runtime: `O(1)`. + /// + /// Space: `O(1)`. + public func singleton(element : T) : Queue = #one(element); + + /// Determine the number of elements contained in a queue. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.singleton(42); + /// assert Queue.size(queue) == 1; + /// } + /// ``` + /// + /// Runtime: `O(1)`. + /// + /// Space: `O(1)`. + public func size(queue : Queue) : Nat = switch queue { + case (#empty) 0; + case (#one _) 1; + case (#two _) 2; + case (#three _) 3; + case (#idles((l, nL), (r, nR))) { + debug assert Stacks.size(l) == nL and Stacks.size(r) == nR; + nL + nR + }; + case (#rebal((_, big, small))) BigState.size(big) + SmallState.size(small) + }; + + /// Test if a queue contains a given value. + /// Returns true if the queue contains the item, otherwise false. + /// + /// Note: The order in which elements are visited is undefined, for performance reasons. + /// + /// Example: + /// ```motoko include=import + /// import Nat "mo:base/Nat"; + /// + /// persistent actor { + /// let queue = Queue.pushBack(Queue.pushBack(Queue.empty(), 1), 2); + /// assert Queue.contains(queue, Nat.equal, 1); + /// assert not Queue.contains(queue, Nat.equal, 3); + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(1)` + public func contains(queue : Queue, eq : (T, T) -> Bool, item : T) : Bool = switch queue { + case (#empty) false; + case (#one(x)) eq(x, item); + case (#two(x, y)) eq(x, item) or eq(y, item); + case (#three(x, y, z)) eq(x, item) or eq(y, item) or eq(z, item); + case (#idles(((l1, l2), _), ((r1, r2), _))) List.contains(l1, eq, item) or List.contains(l2, eq, item) or List.contains(r2, eq, item) or List.contains(r1, eq, item); // note that the order of the right stack is reversed, but for this operation it does not matter + case (#rebal((_, big, small))) { + let (extraB, _, (oldB1, oldB2), _) = BigState.current(big); + let (extraS, _, (oldS1, oldS2), _) = SmallState.current(small); + // note that the order of one of the stacks is reversed (depending on the `direction` field), but for this operation it does not matter + List.contains(extraB, eq, item) or List.contains(oldB1, eq, item) or List.contains(oldB2, eq, item) or List.contains(extraS, eq, item) or List.contains(oldS1, eq, item) or List.contains(oldS2, eq, item) + } + }; + + /// Inspect the optional element on the front end of a queue. + /// Returns `null` if `queue` is empty. Otherwise, the front element of `queue`. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.pushFront(Queue.pushFront(Queue.empty(), 2), 1); + /// assert Queue.peekFront(queue) == ?1; + /// } + /// ``` + /// + /// Runtime: `O(1)`. + /// + /// Space: `O(1)`. + public func peekFront(queue : Queue) : ?T = switch queue { + case (#idles((l, _), _)) Stacks.first(l); + case (#rebal((dir, big, small))) switch dir { + case (#left) ?SmallState.peek(small); + case (#right) ?BigState.peek(big) + }; + case (#empty) null; + case (#one(x)) ?x; + case (#two(x, _)) ?x; + case (#three(x, _, _)) ?x; + }; + + /// Inspect the optional element on the back end of a queue. + /// Returns `null` if `queue` is empty. Otherwise, the back element of `queue`. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.pushFront(Queue.pushFront(Queue.empty(), 2), 1); + /// assert Queue.peekBack(queue) == ?2; + /// } + /// ``` + /// + /// Runtime: `O(1)`. + /// + /// Space: `O(1)`. + public func peekBack(queue : Queue) : ?T = switch queue { + case (#empty) null; + case (#one(x)) ?x; + case (#two(_, y)) ?y; + case (#three(_, _, z)) ?z; + case (#idles(_, (r, _))) Stacks.first(r); + case (#rebal((dir, big, small))) switch dir { + case (#left) ?BigState.peek(big); + case (#right) ?SmallState.peek(small) + } + }; + + /// Insert a new element on the front end of a queue. + /// Returns the new queue with `element` in the front followed by the elements of `queue`. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.pushFront(Queue.pushFront(Queue.empty(), 2), 1); + /// assert Queue.peekFront(queue) == ?1; + /// assert Queue.peekBack(queue) == ?2; + /// assert Queue.size(queue) == 2; + /// } + /// ``` + /// + /// Runtime: `O(1)` worst-case! + /// + /// Space: `O(1)` worst-case! + public func pushFront(queue : Queue, element : T) : Queue = switch queue { + case (#idles(l0, rnR)) { + let lnL = Idle.push(l0, element); // enque the element to the left end + // check if the size invariant still holds + if (3 * rnR.1 >= lnL.1) { + debug assert 3 * lnL.1 >= rnR.1; + #idles(lnL, rnR) + } else { + let (l, nL) = lnL; + let (r, nR) = rnR; + // initiate the rebalancing process + let targetSizeL = nL - nR - 1 : Nat; + let targetSizeR = 2 * nR + 1; + debug assert targetSizeL + targetSizeR == nL + nR; + let big = #big1(Current.new(l, targetSizeL), l, null, targetSizeL); + let small = #small1(Current.new(r, targetSizeR), r, null); + let states = (#right, big, small); + let states6 = States.step(States.step(States.step(States.step(States.step(States.step(states)))))); + #rebal(states6) + } + }; + // if the queue is in the middle of a rebalancing process: push the element and advance the rebalancing process by 4 steps + // move back into the idle state if the rebalancing is done + case (#rebal((dir, big0, small0))) switch dir { + case (#right) { + let big = BigState.push(big0, element); + let states4 = States.step(States.step(States.step(States.step((#right, big, small0))))); + debug assert states4.0 == #right; + switch states4 { + case (_, #big2(#idle(_, big)), #small3(#idle(_, small))) { + debug assert idlesInvariant(big, small); + #idles(big, small) + }; + case _ #rebal(states4) + } + }; + case (#left) { + let small = SmallState.push(small0, element); + let states4 = States.step(States.step(States.step(States.step((#left, big0, small))))); + debug assert states4.0 == #left; + switch states4 { + case (_, #big2(#idle(_, big)), #small3(#idle(_, small))) { + debug assert idlesInvariant(small, big); + #idles(small, big) // swapped because dir=left + }; + case _ #rebal(states4) + } + } + }; + case (#empty) #one(element); + case (#one(y)) #two(element, y); + case (#two(y, z)) #three(element, y, z); + case (#three(a, b, c)) { + let i1 = ((?(element, ?(a, null)), null), 2); + let i2 = ((?(c, ?(b, null)), null), 2); + #idles(i1, i2) + }; + }; + + /// Insert a new element on the back end of a queue. + /// Returns the new queue with all the elements of `queue`, followed by `element` on the back. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.pushBack(Queue.pushBack(Queue.empty(), 1), 2); + /// assert Queue.peekBack(queue) == ?2; + /// assert Queue.size(queue) == 2; + /// } + /// ``` + /// + /// Runtime: `O(1)` worst-case! + /// + /// Space: `O(1)` worst-case! + public func pushBack(queue : Queue, element : T) : Queue = reverse(pushFront(reverse(queue), element)); + + /// Remove the element on the front end of a queue. + /// Returns `null` if `queue` is empty. Otherwise, it returns a pair of + /// the first element and a new queue that contains all the remaining elements of `queue`. + /// + /// Example: + /// ```motoko include=import + /// import Runtime "mo:base/Runtime"; + /// + /// persistent actor { + /// do { + /// let initial = Queue.pushBack(Queue.pushBack(Queue.empty(), 1), 2); + /// let ?(frontElement, remainingQueue) = Queue.popFront(initial) else Runtime.trap "Empty queue impossible"; + /// assert frontElement == 1; + /// assert Queue.size(remainingQueue) == 1; + /// } + /// } + /// ``` + /// + /// Runtime: `O(1)` worst-case! + /// + /// Space: `O(1)` worst-case! + public func popFront(queue : Queue) : ?(T, Queue) = switch queue { + case (#idles(l0, rnR)) { + let (x, lnL) = Idle.pop(l0); + if (3 * lnL.1 >= rnR.1) { + ?(x, #idles(lnL, rnR)) + } else if (lnL.1 >= 1) { + let (l, nL) = lnL; + let (r, nR) = rnR; + let targetSizeL = 2 * nL + 1; + let targetSizeR = nR - nL - 1 : Nat; + debug assert targetSizeL + targetSizeR == nL + nR; + let small = #small1(Current.new(l, targetSizeL), l, null); + let big = #big1(Current.new(r, targetSizeR), r, null, targetSizeR); + let states = (#left, big, small); + let states6 = States.step(States.step(States.step(States.step(States.step(States.step(states)))))); + ?(x, #rebal(states6)) + } else { + ?(x, Stacks.smallqueue(rnR.0)) + } + }; + case (#rebal((dir, big0, small0))) switch dir { + case (#left) { + let (x, small) = SmallState.pop(small0); + let states4 = States.step(States.step(States.step(States.step((#left, big0, small))))); + debug assert states4.0 == #left; + switch states4 { + case (_, #big2(#idle(_, big)), #small3(#idle(_, small))) { + debug assert idlesInvariant(small, big); + ?(x, #idles(small, big)) + }; + case _ ?(x, #rebal(states4)) + } + }; + case (#right) { + let (x, big) = BigState.pop(big0); + let states4 = States.step(States.step(States.step(States.step((#right, big, small0))))); + debug assert states4.0 == #right; + switch states4 { + case (_, #big2(#idle(_, big)), #small3(#idle(_, small))) { + debug assert idlesInvariant(big, small); + ?(x, #idles(big, small)) + }; + case _ ?(x, #rebal(states4)) + } + }; + }; + case (#empty) null; + case (#one(x)) ?(x, #empty); + case (#two(x, y)) ?(x, #one(y)); + case (#three(x, y, z)) ?(x, #two(y, z)); + }; + + /// Remove the element on the back end of a queue. + /// Returns `null` if `queue` is empty. Otherwise, it returns a pair of + /// a new queue that contains the remaining elements of `queue` + /// and, as the second pair item, the removed back element. + /// + /// Example: + /// ```motoko include=import + /// import Runtime "mo:base/Runtime"; + /// + /// persistent actor { + /// do { + /// let initial = Queue.pushBack(Queue.pushBack(Queue.empty(), 1), 2); + /// let ?(reducedQueue, removedElement) = Queue.popBack(initial) else Runtime.trap "Empty queue impossible"; + /// assert removedElement == 2; + /// assert Queue.size(reducedQueue) == 1; + /// } + /// } + /// ``` + /// + /// Runtime: `O(1)` worst-case! + /// + /// Space: `O(1)` worst-case! + public func popBack(queue : Queue) : ?(Queue, T) = do ? { + let (x, queue2) = popFront(reverse(queue))!; + (reverse(queue2), x) + }; + + /// Turn an iterator into a queue, consuming it. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.fromIter([0, 1, 2, 3, 4].values()); + /// assert Queue.peekFront(queue) == ?0; + /// assert Queue.peekBack(queue) == ?4; + /// assert Queue.size(queue) == 5; + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` + public func fromIter(iter : Iter) : Queue { + var queue = empty(); + Iter.forEach(iter, func(t : T) = queue := pushBack(queue, t)); + queue + }; + + /// Create an iterator over the elements in the queue. The order of the elements is from front to back. + /// + /// Example: + /// ```motoko include=import + /// import Iter "mo:base/Iter"; + /// + /// persistent actor { + /// let queue = Queue.fromIter([1, 2, 3].values()); + /// assert Iter.toArray(Queue.values(queue)) == [1, 2, 3]; + /// } + /// ``` + /// + /// Runtime: `O(1)` to create the iterator and for each `next()` call. + /// + /// Space: `O(1)` to create the iterator and for each `next()` call. + public func values(queue : Queue) : Iter.Iter { + object { + var current = queue; + public func next() : ?T { + switch (popFront(current)) { + case null null; + case (?result) { + current := result.1; + ?result.0 + } + } + } + } + }; + + /// Compare two queues for equality using a provided equality function to compare their elements. + /// Two queues are considered equal if they contain the same elements in the same order. + /// + /// Example: + /// ```motoko include=import + /// import Nat "mo:base/Nat"; + /// + /// persistent actor { + /// let queue1 = Queue.fromIter([1, 2, 3].values()); + /// let queue2 = Queue.fromIter([1, 2, 3].values()); + /// let queue3 = Queue.fromIter([1, 3, 2].values()); + /// assert Queue.equal(queue1, queue2, Nat.equal); + /// assert not Queue.equal(queue1, queue3, Nat.equal); + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` + public func equal(queue1 : Queue, queue2 : Queue, equality : (T, T) -> Bool) : Bool { + if (size(queue1) != size(queue2)) { + return false + }; + func go(queue1 : Queue, queue2 : Queue, equality : (T, T) -> Bool) : Bool = switch (popFront queue1, popFront queue2) { + case (null, null) true; + case (?(x1, tail1), ?(x2, tail2)) equality(x1, x2) and go(tail1, tail2, equality); // Note that this is tail recursive (`and` is expanded to `if`). + case _ false + }; + go(queue1, queue2, equality) + }; + + /// Compare two queues lexicographically using a provided comparison function to compare their elements. + /// Returns `#less` if `queue1` is lexicographically less than `queue2`, `#equal` if they are equal, and `#greater` otherwise. + /// + /// Example: + /// ```motoko include=import + /// import Nat "mo:base/Nat"; + /// + /// persistent actor { + /// let queue1 = Queue.fromIter([1, 2, 3].values()); + /// let queue2 = Queue.fromIter([1, 2, 4].values()); + /// assert Queue.compare(queue1, queue2, Nat.compare) == #less; + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` + public func compare(queue1 : Queue, queue2 : Queue, comparison : (T, T) -> Types.Order) : Types.Order = switch (popFront queue1, popFront queue2) { + case (null, null) #equal; + case (null, _) #less; + case (_, null) #greater; + case (?(x1, queue1Tail), ?(x2, queue2Tail)) { + switch (comparison(x1, x2)) { + case (#equal) compare(queue1Tail, queue2Tail, comparison); + case order order + } + } + }; + + /// Return true if the given predicate is true for all queue elements. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.fromIter([2, 4, 6].values()); + /// assert Queue.all(queue, func n = n % 2 == 0); + /// assert not Queue.all(queue, func n = n > 4); + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` as the current implementation uses `values` to iterate over the queue. + /// + /// *Runtime and space assumes that the `predicate` runs in `O(1)` time and space. + public func all(queue : Queue, predicate : T -> Bool) : Bool = switch queue { + case (#empty) true; + case (#one(x)) predicate x; + case (#two(x, y)) predicate x and predicate y; + case (#three(x, y, z)) predicate x and predicate y and predicate z; + case _ { + for (item in values queue) if (not (predicate item)) return false; + return true + } + }; + + /// Return true if the given predicate is true for any queue element. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.fromIter([1, 2, 3].values()); + /// assert Queue.any(queue, func n = n > 2); + /// assert not Queue.any(queue, func n = n > 3); + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` as the current implementation uses `values` to iterate over the queue. + /// + /// *Runtime and space assumes that the `predicate` runs in `O(1)` time and space. + public func any(queue : Queue, predicate : T -> Bool) : Bool = switch queue { + case (#empty) false; + case (#one(x)) predicate x; + case (#two(x, y)) predicate x or predicate y; + case (#three(x, y, z)) predicate x or predicate y or predicate z; + case _ { + for (item in values queue) if (predicate item) return true; + return false + } + }; + + /// Call the given function for its side effect on each queue element in order: from front to back. + /// + /// Example: + /// ```motoko include=import + /// import Nat "mo:base/Nat"; + /// persistent actor { + /// var text = ""; + /// let queue = Queue.fromIter([1, 2, 3].values()); + /// Queue.forEach(queue, func n = text #= Nat.toText(n)); + /// assert text == "123"; + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` + /// + /// *Runtime and space assumes that `f` runs in `O(1)` time and space. + public func forEach(queue : Queue, f : T -> ()) = switch queue { + case (#empty) (); + case (#one(x)) f x; + case (#two(x, y)) { f x; f y }; + case (#three(x, y, z)) { f x; f y; f z }; + // Preserve the order when visiting the elements. Note that the #idles case would require reversing the second stack. + case _ { + for (t in values queue) f t + } + }; + + /// Create a new queue by applying the given function to each element of the original queue. + /// + /// Note: The order of visiting elements is undefined with the current implementation. + /// + /// Example: + /// ```motoko include=import + /// import Nat "mo:base/Nat"; + /// + /// persistent actor { + /// let queue = Queue.fromIter([1, 2, 3].values()); + /// let mapped = Queue.map(queue, func n = n * 2); + /// assert Queue.size(mapped) == 3; + /// assert Queue.peekFront(mapped) == ?2; + /// assert Queue.peekBack(mapped) == ?6; + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` + /// + /// *Runtime and space assumes that `f` runs in `O(1)` time and space. + public func map(queue : Queue, f : T1 -> T2) : Queue = switch queue { + case (#empty) #empty; + case (#one(x)) #one(f x); + case (#two(x, y)) #two(f x, f y); + case (#three(x, y, z)) #three(f x, f y, f z); + case (#idles(l, r)) #idles(Idle.map(l, f), Idle.map(r, f)); + case (#rebal(_)) { + // No reason to rebuild the #rebal state. + // future work: It could be further optimized by building a balanced #idles state directly since we know the sizes. + var q = empty(); + for (t in values queue) q := pushBack(q, f t); + q + } + }; + + /// Create a new queue with only those elements of the original queue for which + /// the given predicate returns true. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.fromIter([1, 2, 3, 4].values()); + /// let filtered = Queue.filter(queue, func n = n % 2 == 0); + /// assert Queue.size(filtered) == 2; + /// assert Queue.peekFront(filtered) == ?2; + /// assert Queue.peekBack(filtered) == ?4; + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` + /// + /// *Runtime and space assumes that `predicate` runs in `O(1)` time and space. + public func filter(queue : Queue, predicate : T -> Bool) : Queue { + var q = empty(); + for (t in values queue) if (predicate t) q := pushBack(q, t); + q + }; + + /// Create a new queue by applying the given function to each element of the original queue + /// and collecting the results for which the function returns a non-null value. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.fromIter([1, 2, 3, 4].values()); + /// let filtered = Queue.filterMap(queue, func n = if (n % 2 == 0) { ?n } else null); + /// assert Queue.size(filtered) == 2; + /// assert Queue.peekFront(filtered) == ?2; + /// assert Queue.peekBack(filtered) == ?4; + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` + /// + /// *Runtime and space assumes that f runs in `O(1)` time and space. + public func filterMap(queue : Queue, f : T -> ?U) : Queue { + var q = empty(); + for (t in values queue) { + switch (f t) { + case (?x) q := pushBack(q, x); + case null () + } + }; + q + }; + + /// Create a `Text` representation of a queue for debugging purposes. + /// + /// Example: + /// ```motoko include=import + /// import Nat "mo:base/Nat"; + /// + /// persistent actor { + /// let queue = Queue.fromIter([1, 2, 3].values()); + /// assert Queue.toText(queue, Nat.toText) == "RealTimeQueue[1, 2, 3]"; + /// } + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` + /// + /// *Runtime and space assumes that f runs in `O(1)` time and space. + public func toText(queue : Queue, f : T -> Text) : Text { + var text = "RealTimeQueue["; + var first = true; + for (t in values queue) { + if (first) first := false else text #= ", "; + text #= f(t) + }; + text # "]" + }; + + /// Reverse the order of elements in a queue. + /// This operation is cheap, it does NOT require copying the elements. + /// + /// Example: + /// ```motoko include=import + /// persistent actor { + /// let queue = Queue.fromIter([1, 2, 3].values()); + /// let reversed = Queue.reverse(queue); + /// assert Queue.peekFront(reversed) == ?3; + /// assert Queue.peekBack(reversed) == ?1; + /// } + /// ``` + /// + /// Runtime: `O(1)` + /// + /// Space: `O(1)` + public func reverse(queue : Queue) : Queue = switch queue { + case (#idles(l, r)) #idles(r, l); + case (#rebal(#left, big, small)) #rebal(#right, big, small); + case (#rebal(#right, big, small)) #rebal(#left, big, small); + case (#empty) queue; + case (#one(_)) queue; + case (#two(x, y)) #two(y, x); + case (#three(x, y, z)) #three(z, y, x) + }; + + type Stacks = (left : List, right : List); + + module Stacks { + public func push((left, right) : Stacks, t : T) : Stacks = (?(t, left), right); + + public func pop(stacks : Stacks) : Stacks = switch stacks { + case (?(_, leftTail), right) (leftTail, right); + case (null, ?(_, rightTail)) (null, rightTail); + case (null, null) stacks + }; + + public func first((left, right) : Stacks) : ?T = switch (left) { + case (?(h, _)) ?h; + case (null) do ? { right!.0 } + }; + + public func unsafeFirst((left, right) : Stacks) : T = switch (left) { + case (?(h, _)) h; + case (null) Option.unwrap(right).0 + }; + + public func isEmpty((left, right) : Stacks) : Bool = List.isEmpty(left) and List.isEmpty(right); + + public func size((left, right) : Stacks) : Nat = List.size(left) + List.size(right); + + public func smallqueue((left, right) : Stacks) : Queue = switch (left, right) { + case (null, null) #empty; + case (null, ?(x, null)) #one(x); + case (?(x, null), null) #one(x); + case (null, ?(x, ?(y, null))) #two(y, x); + case (?(x, null), ?(y, null)) #two(y, x); + case (?(x, ?(y, null)), null) #two(y, x); + case (null, ?(x, ?(y, ?(z, null)))) #three(z, y, x); + case (?(x, ?(y, ?(z, null))), null) #three(z, y, x); + case (?(x, ?(y, null)), ?(z, null)) #three(z, y, x); + case (?(x, null), ?(y, ?(z, null))) #three(z, y, x); + case _ (trap "Queue.Stacks.smallqueue() impossible") + }; + + public func map((left, right) : Stacks, f : T -> U) : Stacks = (List.map(left, f), List.map(right, f)) + }; + + /// Represents an end of the queue that is not in a rebalancing process. It is a stack and its size. + type Idle = (stacks : Stacks, size : Nat); + module Idle { + public func push((stacks, size) : Idle, t : T) : Idle = (Stacks.push(stacks, t), 1 + size); + public func pop((stacks, size) : Idle) : (T, Idle) = (Stacks.unsafeFirst(stacks), (Stacks.pop(stacks), size - 1 : Nat)); + public func peek((stacks, _) : Idle) : T = Stacks.unsafeFirst(stacks); + + public func map((stacks, size) : Idle, f : T -> U) : Idle = (Stacks.map(stacks, f), size) + }; + + /// Stores information about operations that happen during rebalancing but which have not become part of the old state that is being rebalanced. + /// + /// - `extra`: newly added elements + /// - `extraSize`: size of `extra` + /// - `old`: elements contained before the rebalancing process + /// - `targetSize`: the number of elements which will be contained after the rebalancing is finished + type Current = (extra : List, extraSize : Nat, old : Stacks, targetSize : Nat); + + module Current { + public func new(old : Stacks, targetSize : Nat) : Current = (null, 0, old, targetSize); + + public func push((extra, extraSize, old, targetSize) : Current, t : T) : Current = (?(t, extra), 1 + extraSize, old, targetSize); + + public func pop((extra, extraSize, old, targetSize) : Current) : (T, Current) = switch (extra) { + case (?(h, t)) (h, (t, extraSize - 1 : Nat, old, targetSize)); + case (null) (Stacks.unsafeFirst(old), (null, extraSize, Stacks.pop(old), targetSize - 1 : Nat)) + }; + + public func peek((extra, _, old, _) : Current) : T = switch (extra) { + case (?(h, _)) h; + case (null) Stacks.unsafeFirst(old) + }; + + public func size((_, extraSize, _, targetSize) : Current) : Nat = extraSize + targetSize + }; + + /// The bigger end of the queue during rebalancing. It is used to split the bigger end of the queue into the new big end and a portion to be added to the small end. Can be in one of the following states: + /// + /// - `#big1(cur, big, aux, n)`: Initial stage. Using the step function it takes `n`-elements from the `big` stack and puts them to `aux` in reversed order. `#big1(cur, x1 .. xn : bigTail, [], n) ->* #big1(cur, bigTail, xn .. x1, 0)`. The `bigTail` is later given to the `small` end. + /// - `#big2(common)`: Is used to reverse the elements from the previous phase to restore the original order. `common = #copy(cur, xn .. x1, [], 0) ->* #copy(cur, [], x1 .. xn, n)`. + type BigState = { + #big1 : (Current, Stacks, List, Nat); + #big2 : CommonState + }; + + module BigState { + public func push(big : BigState, t : T) : BigState = switch big { + case (#big1(cur, big, aux, n)) #big1(Current.push(cur, t), big, aux, n); + case (#big2(state)) #big2(CommonState.push(state, t)) + }; + + public func pop(big : BigState) : (T, BigState) = switch big { + case (#big1(cur, big, aux, n)) { + let (x, cur2) = Current.pop(cur); + (x, #big1(cur2, big, aux, n)) + }; + case (#big2(state)) { + let (x, state2) = CommonState.pop(state); + (x, #big2(state2)) + } + }; + + public func peek(big : BigState) : T = switch big { + case (#big1(cur, _, _, _)) Current.peek(cur); + case (#big2(state)) CommonState.peek(state) + }; + + public func step(big : BigState) : BigState = switch big { + case (#big1(cur, big, aux, n)) { + if (n == 0) + #big2(CommonState.norm(#copy(cur, aux, null, 0))) else + #big1(cur, Stacks.pop(big), ?(Stacks.unsafeFirst(big), aux), n - 1 : Nat) + }; + case (#big2(state)) #big2(CommonState.step(state)) + }; + + public func size(big : BigState) : Nat = switch big { + case (#big1(cur, _, _, _)) Current.size(cur); + case (#big2(state)) CommonState.size(state) + }; + + public func current(big : BigState) : Current = switch big { + case (#big1(cur, _, _, _)) cur; + case (#big2(state)) CommonState.current(state) + } + }; + + /// The smaller end of the queue during rebalancing. Can be in one of the following states: + /// + /// - `#small1(cur, small, aux)`: Initial stage. Using the step function the original elements are reversed. `#small1(cur, s1 .. sn, []) ->* #small1(cur, [], sn .. s1)`, note that `aux` is initially empty, at the end contains the reversed elements from the small stack. + /// - `#small2(cur, aux, big, new, size)`: Using the step function the newly transfered tail from the bigger end is reversed on top of the `new` list. `#small2(cur, sn .. s1, b1 .. bm, [], 0) ->* #small2(cur, sn .. s1, [], bm .. b1, m)`, note that `aux` is the reversed small stack from the previous phase, `new` is initially empty, `size` corresponds to the size of `new`. + /// - `#small3(common)`: Is used to reverse the elements from the two previous phases again to get them again in the original order. `#copy(cur, sn .. s1, bm .. b1, m) ->* #copy(cur, [], s1 .. sn : bm .. b1, n + m)`, note that the correct order of the elements from the big stack is reversed. + type SmallState = { + #small1 : (Current, Stacks, List); + #small2 : (Current, List, Stacks, List, Nat); + #small3 : CommonState + }; + + module SmallState { + public func push(state : SmallState, t : T) : SmallState = switch state { + case (#small1(cur, small, aux)) #small1(Current.push(cur, t), small, aux); + case (#small2(cur, aux, big, new, newN)) #small2(Current.push(cur, t), aux, big, new, newN); + case (#small3(common)) #small3(CommonState.push(common, t)) + }; + + public func pop(state : SmallState) : (T, SmallState) = switch state { + case (#small1(cur0, small, aux)) { + let (t, cur) = Current.pop(cur0); + (t, #small1(cur, small, aux)) + }; + case (#small2(cur0, aux, big, new, newN)) { + let (t, cur) = Current.pop(cur0); + (t, #small2(cur, aux, big, new, newN)) + }; + case (#small3(common0)) { + let (t, common) = CommonState.pop(common0); + (t, #small3(common)) + } + }; + + public func peek(state : SmallState) : T = switch state { + case (#small1(cur, _, _)) Current.peek(cur); + case (#small2(cur, _, _, _, _)) Current.peek(cur); + case (#small3(common)) CommonState.peek(common) + }; + + public func step(state : SmallState) : SmallState = switch state { + case (#small1(cur, small, aux)) { + if (Stacks.isEmpty(small)) state else #small1(cur, Stacks.pop(small), ?(Stacks.unsafeFirst(small), aux)) + }; + case (#small2(cur, aux, big, new, newN)) { + if (Stacks.isEmpty(big)) #small3(CommonState.norm(#copy(cur, aux, new, newN))) else #small2(cur, aux, Stacks.pop(big), ?(Stacks.unsafeFirst(big), new), 1 + newN) + }; + case (#small3(common)) #small3(CommonState.step(common)) + }; + + public func size(state : SmallState) : Nat = switch state { + case (#small1(cur, _, _)) Current.size(cur); + case (#small2(cur, _, _, _, _)) Current.size(cur); + case (#small3(common)) CommonState.size(common) + }; + + public func current(state : SmallState) : Current = switch state { + case (#small1(cur, _, _)) cur; + case (#small2(cur, _, _, _, _)) cur; + case (#small3(common)) CommonState.current(common) + } + }; + + type CopyState = { #copy : (Current, List, List, Nat) }; + + /// Represents the last rebalancing phase of both small and big ends of the queue. It is used to reverse the elements from the previous phases to restore the original order. It can be in one of the following states: + /// + /// - `#copy(cur, aux, new, sizeOfNew)`: Puts the elements from `aux` in reversed order on top of `new`. `#copy(cur, xn .. x1, new, sizeOfNew) ->* #copy(cur, [], x1 .. xn : new, n + sizeOfNew)`. + /// - `#idle(cur, idle)`: The rebalancing process is done and the queue is in the idle state. + type CommonState = CopyState or { #idle : (Current, Idle) }; + + module CommonState { + public func step(common : CommonState) : CommonState = switch common { + case (#copy copy) { + let (cur, aux, new, sizeOfNew) = copy; + let (_, _, _, targetSize) = cur; + norm(if (sizeOfNew < targetSize) #copy(cur, unsafeTail(aux), ?(unsafeHead(aux), new), 1 + sizeOfNew) else #copy copy) + }; + case (#idle _) common + }; + + public func norm(copy : CopyState) : CommonState { + let #copy(cur, _, new, sizeOfNew) = copy; + let (extra, extraSize, _, targetSize) = cur; + debug assert sizeOfNew <= targetSize; + if (sizeOfNew >= targetSize) { + #idle(cur, ((extra, new), extraSize + sizeOfNew)) // note: aux can be non-empty, thus ignored here, when the target size decreases after pop operations + } else copy + }; + + public func push(common : CommonState, t : T) : CommonState = switch common { + case (#copy(cur, aux, new, sizeOfNew)) #copy(Current.push(cur, t), aux, new, sizeOfNew); + case (#idle(cur, idle)) #idle(Current.push(cur, t), Idle.push(idle, t)) // yes, push to both + }; + + public func pop(common : CommonState) : (T, CommonState) = switch common { + case (#copy(cur, aux, new, sizeOfNew)) { + let (t, cur2) = Current.pop(cur); + (t, norm(#copy(cur2, aux, new, sizeOfNew))) + }; + case (#idle(cur, idle)) { + let (t, idle2) = Idle.pop(idle); + (t, #idle(Current.pop(cur).1, idle2)) + } + }; + + public func peek(common : CommonState) : T = switch common { + case (#copy(cur, _, _, _)) Current.peek(cur); + case (#idle(_, idle)) Idle.peek(idle) + }; + + public func size(common : CommonState) : Nat = switch common { + case (#copy(cur, _, _, _)) Current.size(cur); + case (#idle(_, (_, size))) size + }; + + public func current(common : CommonState) : Current = switch common { + case (#copy(cur, _, _, _)) cur; + case (#idle(cur, _)) cur + } + }; + + type States = ( + direction : Direction, + bigState : BigState, + smallState : SmallState + ); + + module States { + public func step(states : States) : States = switch states { + case (dir, #big1(_, bigTail, _, 0), #small1(currentS, _, auxS)) { + (dir, BigState.step(states.1), #small2(currentS, auxS, bigTail, null, 0)) + }; + case (dir, big, small) (dir, BigState.step(big), SmallState.step(small)) + } + }; + + type Direction = { #left; #right }; + + func idlesInvariant(((l, nL), (r, nR)) : (Idle, Idle)) : Bool = Stacks.size(l) == nL and Stacks.size(r) == nR and 3 * nL >= nR and 3 * nR >= nL; + + type List = Types.Pure.List; + type Iter = Types.Iter; + func unsafeHead(l : List) : T = Option.unwrap(l).0; + func unsafeTail(l : List) : List = Option.unwrap(l).1 +} diff --git a/test/pure/Queue.test.mo b/test/pure/Queue.test.mo index 82449d904..1d50e3171 100644 --- a/test/pure/Queue.test.mo +++ b/test/pure/Queue.test.mo @@ -1,3 +1,5 @@ +// @testmode wasi + import Queue "../../src/pure/Queue"; import Array "../../src/Array"; import Nat "../../src/Nat"; @@ -5,35 +7,9 @@ import Iter "../../src/Iter"; import Prim "mo:prim"; import { suite; test; expect } "mo:test"; -func iterateForward(queue : Queue.Queue) : Iter.Iter { - var current = queue; - object { - public func next() : ?T { - switch (Queue.popFront(current)) { - case null null; - case (?result) { - current := result.1; - ?result.0 - } - } - } - } -}; +func iterateForward(queue : Queue.Queue) : Iter.Iter = Queue.values(queue); -func iterateBackward(queue : Queue.Queue) : Iter.Iter { - var current = queue; - object { - public func next() : ?T { - switch (Queue.popBack(current)) { - case null null; - case (?result) { - current := result.0; - ?result.1 - } - } - } - } -}; +func iterateBackward(queue : Queue.Queue) : Iter.Iter = Queue.values(Queue.reverse(queue)); func frontToText(t : (Nat, Queue.Queue)) : Text { "(" # Nat.toText(t.0) # ", " # Queue.toText(t.1, Nat.toText) # ")" diff --git a/test/pure/RealTimeQueue.test.mo b/test/pure/RealTimeQueue.test.mo new file mode 100644 index 000000000..3521a0239 --- /dev/null +++ b/test/pure/RealTimeQueue.test.mo @@ -0,0 +1,891 @@ +// @testmode wasi + +import Queue "../../src/pure/RealTimeQueue"; +import Array "../../src/Array"; +import Nat "../../src/Nat"; +import Iter "../../src/Iter"; +import Prim "mo:prim"; +import { suite; test; expect } "mo:test"; +import Text "../../src/Text"; + +type Queue = Queue.Queue; + +func iterateForward(queue : Queue) : Iter.Iter = Queue.values(queue); + +func iterateBackward(queue : Queue) : Iter.Iter = Queue.values(Queue.reverse(queue)); + +func frontToText(t : (Nat, Queue)) : Text { + "(" # Nat.toText(t.0) # ", " # Queue.toText(t.1, Nat.toText) # ")" +}; + +func frontEqual(t1 : (Nat, Queue), t2 : (Nat, Queue)) : Bool { + t1.0 == t2.0 and Queue.equal(t1.1, t2.1, Nat.equal) +}; + +func backToText(t : (Queue, Nat)) : Text { + "(" # Queue.toText(t.0, Nat.toText) # ", " # Nat.toText(t.1) # ")" +}; + +func backEqual(t1 : (Queue, Nat), t2 : (Queue, Nat)) : Bool { + t1.1 == t2.1 and Queue.equal(t1.0, t2.0, Nat.equal) +}; + +func reduceFront(queue : Queue, amount : Nat) : Queue { + var current = queue; + for (_ in Nat.range(0, amount)) { + switch (Queue.popFront(current)) { + case null Prim.trap("should not be null"); + case (?result) current := result.1 + } + }; + current +}; + +func reduceBack(queue : Queue, amount : Nat) : Queue { + var current = queue; + for (_ in Nat.range(0, amount)) { + switch (Queue.popBack(current)) { + case null Prim.trap("should not be null"); + case (?result) current := result.0 + } + }; + current +}; + +var queue = Queue.empty(); + +suite( + "construct", + func() { + test( + "empty", + func() { + expect.bool(Queue.isEmpty(queue)).isTrue() + } + ); + + test( + "iterate forward", + func() { + expect.array(Iter.toArray(iterateForward(queue)), Nat.toText, Nat.equal).size(0) + } + ); + + test( + "iterate backward", + func() { + expect.array(Iter.toArray(iterateBackward(queue)), Nat.toText, Nat.equal).size(0) + } + ); + + test( + "peek front", + func() { + expect.option(Queue.peekFront(queue), Nat.toText, Nat.equal).isNull() + } + ); + + test( + "peek back", + func() { + expect.option(Queue.peekBack(queue), Nat.toText, Nat.equal).isNull() + } + ); + + test( + "pop front", + func() { + expect.option( + Queue.popFront(queue), + frontToText, + frontEqual + ).isNull() + } + ); + + test( + "pop back", + func() { + expect.option( + Queue.popBack(queue), + backToText, + backEqual + ).isNull() + } + ) + } +); + +queue := Queue.pushFront(Queue.empty(), 1); + +suite( + "single item", + func() { + test( + "not empty", + func() { + expect.bool(Queue.isEmpty(queue)).isFalse() + } + ); + + test( + "iterate forward", + func() { + expect.array(Iter.toArray(iterateForward(queue)), Nat.toText, Nat.equal).equal([1]) + } + ); + + test( + "iterate backward", + func() { + expect.array(Iter.toArray(iterateBackward(queue)), Nat.toText, Nat.equal).equal([1]) + } + ); + + test( + "peek front", + func() { + expect.option(Queue.peekFront(queue), Nat.toText, Nat.equal).equal(?1) + } + ); + + test( + "peek back", + func() { + expect.option(Queue.peekBack(queue), Nat.toText, Nat.equal).equal(?1) + } + ); + + test( + "pop front", + func() { + expect.option( + Queue.popFront(queue), + frontToText, + frontEqual + ).equal(?(1, Queue.empty())) + } + ); + + test( + "pop back", + func() { + expect.option( + Queue.popBack(queue), + backToText, + backEqual + ).equal(?(Queue.empty(), 1)) + } + ) + } +); + +let testSize = 100; + +func populateForward(from : Nat, to : Nat) : Queue { + var queue = Queue.empty(); + for (number in Nat.range(from, to)) { + queue := Queue.pushFront(queue, number) + }; + queue +}; + +queue := populateForward(1, testSize + 1); + +suite( + "forward insertion", + func() { + test( + "not empty", + func() { + expect.bool(Queue.isEmpty(queue)).isFalse() + } + ); + + test( + "iterate forward", + func() { + expect.array( + Iter.toArray(iterateForward(queue)), + Nat.toText, + Nat.equal + ).equal( + Array.tabulate( + testSize, + func(index : Nat) : Nat { + testSize - index + } + ) + ) + } + ); + + test( + "iterate backward", + func() { + expect.array( + Iter.toArray(iterateBackward(queue)), + Nat.toText, + Nat.equal + ).equal( + Array.tabulate( + testSize, + func(index : Nat) : Nat { + index + 1 + } + ) + ) + } + ); + + test( + "peek front", + func() { + expect.option(Queue.peekFront(queue), Nat.toText, Nat.equal).equal(?testSize) + } + ); + + test( + "peek back", + func() { + expect.option(Queue.peekBack(queue), Nat.toText, Nat.equal).equal(?1) + } + ); + + test( + "pop front", + func() { + expect.option( + Queue.popFront(queue), + frontToText, + frontEqual + ).equal(?(testSize, populateForward(1, testSize))) + } + ); + + test( + "empty after front removal", + func() { + expect.bool(Queue.isEmpty(reduceFront(queue, testSize))).isTrue() + } + ); + + test( + "empty after back removal", + func() { + expect.bool(Queue.isEmpty(reduceBack(queue, testSize))).isTrue() + } + ) + } +); + +func populateBackward(from : Nat, to : Nat) : Queue { + var queue = Queue.empty(); + for (number in Nat.range(from, to)) { + queue := Queue.pushBack(queue, number) + }; + queue +}; + +queue := populateBackward(1, testSize + 1); + +suite( + "backward insertion", + func() { + test( + "not empty", + func() { + expect.bool(Queue.isEmpty(queue)).isFalse() + } + ); + + test( + "iterate forward", + func() { + expect.array( + Iter.toArray(iterateForward(queue)), + Nat.toText, + Nat.equal + ).equal( + Array.tabulate( + testSize, + func(index : Nat) : Nat { + index + 1 + } + ) + ) + } + ); + + test( + "iterate backward", + func() { + expect.array( + Iter.toArray(iterateBackward(queue)), + Nat.toText, + Nat.equal + ).equal( + Array.tabulate( + testSize, + func(index : Nat) : Nat { + testSize - index + } + ) + ) + } + ); + + test( + "peek front", + func() { + expect.option(Queue.peekFront(queue), Nat.toText, Nat.equal).equal(?1) + } + ); + + test( + "peek back", + func() { + expect.option(Queue.peekBack(queue), Nat.toText, Nat.equal).equal(?testSize) + } + ); + + test( + "pop front", + func() { + expect.option( + Queue.popFront(queue), + frontToText, + frontEqual + ).equal(?(1, populateBackward(2, testSize + 1))) + } + ); + + test( + "pop back", + func() { + expect.option( + Queue.popBack(queue), + backToText, + backEqual + ).equal(?(populateBackward(1, testSize), testSize)) + } + ); + + test( + "empty after front removal", + func() { + expect.bool(Queue.isEmpty(reduceFront(queue, testSize))).isTrue() + } + ); + + test( + "empty after back removal", + func() { + expect.bool(Queue.isEmpty(reduceBack(queue, testSize))).isTrue() + } + ) + } +); + +queue := Queue.filter(Queue.fromIter([1, 2, 3, 4, 5].vals()), func n = n < 3); + +suite( + "filter invariants", + func() { + test( + "not empty", + func() { + expect.bool(Queue.isEmpty(queue)).isFalse() + } + ); + + test( + "peek front", + func() { + expect.option(Queue.peekFront(queue), Nat.toText, Nat.equal).equal(?1) + } + ); + + test( + "peek back", + func() { + expect.option(Queue.peekBack(queue), Nat.toText, Nat.equal).equal(?2) + } + ) + } +); + +object Random { + var number = 4711; + public func next() : Int { + number := (123138118391 * number + 133489131) % 9999; + number + } +}; + +func randomPopulate(amount : Nat) : Queue { + var current = Queue.empty(); + for (number in Nat.range(0, amount)) { + current := if (Random.next() % 2 == 0) { + Queue.pushFront(current, Nat.sub(amount, number)) + } else { + Queue.pushBack(current, amount + number) + } + }; + current +}; + +func isSorted(queue : Queue) : Bool { + let array = Iter.toArray(iterateForward(queue)); + let sorted = Array.sort(array, Nat.compare); + Array.equal(array, sorted, Nat.equal) +}; + +func randomRemoval(queue : Queue, amount : Nat) : Queue { + var current = queue; + for (number in Nat.range(0, amount)) { + current := if (Random.next() % 2 == 0) { + let pair = Queue.popFront(current); + switch pair { + case null Prim.trap("should not be null"); + case (?result) result.1 + } + } else { + let pair = Queue.popBack(current); + switch pair { + case null Prim.trap("should not be null"); + case (?result) result.0 + } + } + }; + current +}; + +queue := randomPopulate(testSize); + +suite( + "random insertion", + func() { + test( + "not empty", + func() { + expect.bool(Queue.isEmpty(queue)).isFalse() + } + ); + + test( + "correct order", + func() { + expect.bool(isSorted(queue)).isTrue() + } + ); + + test( + "consistent iteration", + func() { + expect.array( + Iter.toArray(iterateForward(queue)), + Nat.toText, + Nat.equal + ).equal(Array.reverse(Iter.toArray(iterateBackward(queue)))) + } + ); + + test( + "random quarter removal", + func() { + expect.bool(isSorted(randomRemoval(queue, testSize / 4))).isTrue() + } + ); + + test( + "random half removal", + func() { + expect.bool(isSorted(randomRemoval(queue, testSize / 2))).isTrue() + } + ); + + test( + "random three quarter removal", + func() { + expect.bool(isSorted(randomRemoval(queue, testSize * 3 / 4))).isTrue() + } + ); + + test( + "random total removal", + func() { + expect.bool(Queue.isEmpty(randomRemoval(queue, testSize))).isTrue() + } + ) + } +); + +func randomInsertionDeletion(steps : Nat) : Queue { + var current = Queue.empty(); + var size = 0; + for (number in Nat.range(0, steps - 1)) { + let random = Random.next(); + current := switch (random % 4) { + case 0 { + size += 1; + Queue.pushFront(current, Nat.sub(steps, number)) + }; + case 1 { + size += 1; + Queue.pushBack(current, steps + number) + }; + case 2 { + switch (Queue.popFront(current)) { + case null { + assert (size == 0); + current + }; + case (?result) { + size -= 1; + result.1 + } + } + }; + case 3 { + switch (Queue.popBack(current)) { + case null { + assert (size == 0); + current + }; + case (?result) { + size -= 1; + result.0 + } + } + }; + case _ Prim.trap("Impossible case") + }; + assert (isSorted(current)) + }; + current +}; + +suite( + "completely random", + func() { + test( + "random insertion and deletion", + func() { + expect.bool(isSorted(randomInsertionDeletion(1000))).isTrue() + } + ) + } +); + +suite( + "code coverage", + func() { + test( + "singleton", + func() { + let q = Queue.singleton(1); + expect.bool(Queue.isEmpty(q)).isFalse(); + expect.option(Queue.peekFront(q), Nat.toText, Nat.equal).equal(?1); + expect.option(Queue.peekBack(q), Nat.toText, Nat.equal).equal(?1) + } + ); + + test( + "all", + func() { + let testAll = func(testElements : [Nat]) { + let q = Queue.fromIter(testElements.vals()); + expect.bool(Queue.all(q, func n = n > 0)).isTrue(); + expect.bool(Queue.all(q, func n = n < 3)).isFalse() + }; + testAll([4]); + testAll([1, 5]); + testAll([1, 2, 6]); + testAll([1, 2, 3, 4]); + testAll([1, 2, 2, 3, 3, 4]); + testAll([1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9]) + } + ); + + test( + "filterMap", + func() { + let testFilterMap = func(testElements : [Nat]) { + let q = Queue.fromIter(testElements.vals()); + let mapped = Queue.filterMap( + q, + func n = if (n % 2 == 0) ?Nat.toText(n) else null + ); + expect.array( + Iter.toArray(Queue.values(mapped)), + func t = t, + Text.equal + ).equal(Array.filterMap(testElements, func n = if (n % 2 == 0) ?Nat.toText(n) else null)) + }; + testFilterMap([]); + testFilterMap([1]); + testFilterMap([1, 2]); + testFilterMap([1, 2, 3]); + testFilterMap([1, 2, 3, 4]); + testFilterMap([1, 2, 2, 3, 3, 4]); + testFilterMap([1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9]) + } + ); + + test( + "forEach", + func() { + let testForEach = func(testElements : [Nat]) { + let q = Queue.fromIter(testElements.vals()); + var result = ""; + Queue.forEach( + q, + func n { + result #= Nat.toText n + } + ); + expect.text(result).equal(Array.foldLeft(testElements, "", func(acc, n) = acc # Nat.toText n)) + }; + testForEach([]); + testForEach([1]); + testForEach([1, 2]); + testForEach([1, 2, 3]); + testForEach([1, 2, 3, 4]); + testForEach([1, 2, 2, 3, 3, 4]); + testForEach([1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9]) + } + ); + + test( + "toText", + func() { + let testToText = func(testElements : [Nat]) { + let q = Queue.fromIter(testElements.vals()); + expect.text(Queue.toText(q, Nat.toText)).equal("RealTimeQueue" # Array.toText(testElements, Nat.toText)) + }; + testToText([]); + testToText([1]); + testToText([1, 2]); + testToText([1, 2, 3]); + testToText([1, 2, 3, 4]); + testToText([1, 2, 2, 3, 3, 4]); + testToText([1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9]) + } + ); + + test( + "size", + func() { + let testSize = func(testElements : [Nat]) { + let q = Queue.fromIter(testElements.vals()); + expect.nat(Queue.size(q)).equal(testElements.size()) + }; + testSize([]); + testSize([1]); + testSize([1, 2]); + testSize([1, 2, 3]); + testSize([1, 2, 3, 4]); + testSize([1, 2, 2, 3, 3, 4]); + testSize([1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9]) + } + ); + + test( + "contains", + func() { + let testContains = func(testElements : [Nat]) { + let q = Queue.fromIter(testElements.vals()); + let alwaysThere = 1; + let neverThere = 123; + expect.bool(Queue.contains(q, Nat.equal, alwaysThere)).equal(Array.find(testElements, func n = Nat.equal(n, alwaysThere)) != null); + expect.bool(Queue.contains(q, Nat.equal, neverThere)).equal(Array.find(testElements, func n = Nat.equal(n, neverThere)) != null) + }; + testContains([]); + testContains([1]); + testContains([1, 2]); + testContains([1, 2, 3]); + testContains([1, 2, 3, 4]); + testContains([1, 2, 2, 3, 3, 4]); + testContains([1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9]) + } + ); + + test( + "any", + func() { + let testAny = func(testElements : [Nat]) { + let q = Queue.fromIter(testElements.vals()); + expect.bool(Queue.any(q, func n = n > 2)).equal(Array.any(testElements, func n = n > 2)); + expect.bool(Queue.any(q, func n = n > 3)).equal(Array.any(testElements, func n = n > 3)) + }; + testAny([]); + testAny([3]); + testAny([1, 3]); + testAny([1, 2, 3]); + testAny([1, 2, 3, 2]); + testAny([1, 2, 3, 2, 1]); + testAny([1, 2, 0, 2, 1, 3]); + testAny([1, 2, 0, 2, 1, 3, 1]) + } + ); + + test( + "map", + func() { + let testMap = func(testElements : [Nat]) { + let q = Queue.fromIter(testElements.vals()); + let mapped = Queue.map(q, func n = n * 2); + expect.array( + Iter.toArray(Queue.values(mapped)), + Nat.toText, + Nat.equal + ).equal(Array.map(testElements, func n = n * 2)) + }; + testMap([]); + testMap([1]); + testMap([1, 2]); + testMap([1, 2, 3]); + testMap([1, 2, 3, 4]); + testMap([1, 2, 2, 3, 3, 4]); + testMap([1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9]) + } + ); + + test( + "reverse", + func() { + let testReverse = func(testElements : [Nat]) { + let q = Queue.fromIter(testElements.vals()); + let reversed = Queue.reverse(q); + expect.array( + Iter.toArray(Queue.values(reversed)), + Nat.toText, + Nat.equal + ).equal(Array.reverse(testElements)) + }; + + testReverse([]); + testReverse([1]); + testReverse([1, 2]); + testReverse([1, 2, 3]); + testReverse([1, 2, 3, 4]); + testReverse([1, 2, 2, 3, 3, 4]); + testReverse([1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9]) + } + ); + + test( + "compare", + func() { + let testCompare = func(testElements1 : [Nat], testElements2 : [Nat]) { + let q1 = Queue.fromIter(testElements1.vals()); + let q2 = Queue.fromIter(testElements2.vals()); + expect.bool(Queue.compare(q1, q2, Nat.compare) == Array.compare(testElements1, testElements2, Nat.compare)).isTrue() + }; + + testCompare([], []); + testCompare([1], []); + testCompare([], [1]); + testCompare([1], [1]); + testCompare([1], [2]); + testCompare([1, 2], [1]); + testCompare([1], [1, 2]); + testCompare([1, 2], [1, 2]); + testCompare([1, 2], [2, 1]); + testCompare([1, 2, 4], [1, 2, 3]); + testCompare([1, 2, 3], [1, 2, 4]); + testCompare([1, 2, 3], [1, 3, 3]); + testCompare([1, 2, 3], [2, 2, 3]); + testCompare([1, 2, 3], [2, 3, 3]); + testCompare([1, 2, 3], [2, 3, 4]) + } + ) + } +); + +suite( + "edge cases", + func() { + test( + "empty queue operations", + func() { + let q = Queue.empty(); + expect.bool(Queue.isEmpty(q)).isTrue(); + expect.nat(Queue.size(q)).equal(0); + expect.option(Queue.peekFront(q), Nat.toText, Nat.equal).isNull(); + expect.option(Queue.peekBack(q), Nat.toText, Nat.equal).isNull(); + expect.option( + Queue.popFront(q), + frontToText, + frontEqual + ).isNull(); + expect.option( + Queue.popBack(q), + backToText, + backEqual + ).isNull() + } + ); + + test( + "rebalancing threshold", + func() { + // Create a queue that's exactly at the rebalancing threshold + var q = Queue.empty(); + for (i in Nat.range(1, 4)) { + q := Queue.pushFront(q, i) + }; + for (i in Nat.range(5, 12)) { + q := Queue.pushBack(q, i) + }; + let #rebal(_) = q else Prim.trap "Should be in rebalancing state"; + // Test operations during rebalancing + expect.text(Queue.toText(q, Nat.toText)).equal("RealTimeQueue[3, 2, 1, 5, 6, 7, 8, 9, 10, 11]"); + q := Queue.pushFront(q, 0); + q := Queue.pushBack(q, 13); + expect.bool(Queue.isEmpty(q)).isFalse(); + expect.option(Queue.peekFront(q), Nat.toText, Nat.equal).equal(?0); + expect.option(Queue.peekBack(q), Nat.toText, Nat.equal).equal(?13) + } + ); + + test( + "alternating operations", + func() { + var q = Queue.empty(); + // Alternating pushes + for (i in Nat.range(0, 5)) { + q := Queue.pushFront(q, i); + q := Queue.pushBack(q, i + 100) + }; + // Mixed operations + expect.option(Queue.popFront(q), frontToText, frontEqual).equal(?(4, Queue.fromIter([3, 2, 1, 0, 100, 101, 102, 103, 104].vals()))); + q := Queue.pushBack(q, 200); + expect.option(Queue.popBack(q), backToText, backEqual).equal(?(Queue.fromIter([4, 3, 2, 1, 0, 100, 101, 102, 103, 104].vals()), 200)); + q := Queue.pushFront(q, 300); + // Verify order is maintained + expect.array( + Iter.toArray(Queue.values(q)), + Nat.toText, + Nat.equal + ).equal([300, 4, 3, 2, 1, 0, 100, 101, 102, 103, 104, 200]) + } + ) + } +); + +suite( + "regression: stack overflow should not happen when iterating over large queues", + func() { + let queue = populateForward(1, 17_001); + let actual = Iter.toArray(iterateBackward(queue)); + expect.array( + actual, + Nat.toText, + Nat.equal + ).equal( + Array.tabulate(17_000, func i = i + 1) + ) + } +) diff --git a/validation/api/api.lock.json b/validation/api/api.lock.json index 64c17ffa0..6110762ac 100644 --- a/validation/api/api.lock.json +++ b/validation/api/api.lock.json @@ -1340,7 +1340,7 @@ "public func contains(queue : Queue, equal : (T, T) -> Bool, item : T) : Bool", "public func empty() : Queue", "public func equal(queue1 : Queue, queue2 : Queue, equal : (T, T) -> Bool) : Bool", - "public func filter(queue : Queue, f : T -> Bool) : Queue", + "public func filter(queue : Queue, predicate : T -> Bool) : Queue", "public func filterMap(queue : Queue, f : T -> ?U) : Queue", "public func forEach(queue : Queue, f : T -> ())", "public func fromIter(iter : Iter.Iter) : Queue", @@ -1353,12 +1353,42 @@ "public func pushBack(queue : Queue, element : T) : Queue", "public func pushFront(queue : Queue, element : T) : Queue", "public type Queue", + "public func reverse(queue : Queue) : Queue", "public func singleton(item : T) : Queue", "public func size(queue : Queue) : Nat", "public func toText(queue : Queue, f : T -> Text) : Text", "public func values(queue : Queue) : Iter.Iter" ] }, + { + "name": "pure/RealTimeQueue", + "exports": [ + "public func all(queue : Queue, predicate : T -> Bool) : Bool", + "public func any(queue : Queue, predicate : T -> Bool) : Bool", + "public func compare(queue1 : Queue, queue2 : Queue, comparison : (T, T) -> Types.Order) : Types.Order", + "public func contains(queue : Queue, eq : (T, T) -> Bool, item : T) : Bool", + "public func empty() : Queue", + "public func equal(queue1 : Queue, queue2 : Queue, equality : (T, T) -> Bool) : Bool", + "public func filter(queue : Queue, predicate : T -> Bool) : Queue", + "public func filterMap(queue : Queue, f : T -> ?U) : Queue", + "public func forEach(queue : Queue, f : T -> ())", + "public func fromIter(iter : Iter) : Queue", + "public func isEmpty(queue : Queue) : Bool", + "public func map(queue : Queue, f : T1 -> T2) : Queue", + "public func peekBack(queue : Queue) : ?T", + "public func peekFront(queue : Queue) : ?T", + "public func popBack(queue : Queue) : ?(Queue, T)", + "public func popFront(queue : Queue) : ?(T, Queue)", + "public func pushBack(queue : Queue, element : T) : Queue", + "public func pushFront(queue : Queue, element : T) : Queue", + "public type Queue", + "public func reverse(queue : Queue) : Queue", + "public func singleton(element : T) : Queue", + "public func size(queue : Queue) : Nat", + "public func toText(queue : Queue, f : T -> Text) : Text", + "public func values(queue : Queue) : Iter.Iter" + ] + }, { "name": "pure/Set", "exports": [