diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..137b8fb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: "ci" + +on: [push, pull_request] + +jobs: + ci: + name: "ci" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.3.4 + - uses: cachix/install-nix-action@v13 + with: + nix_path: nixpkgs=channel:nixos-21.05 + - uses: cachix/cachix-action@v10 + with: + name: ninegua + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + - run: nix-shell --run 'make -C test' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e29584c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Paul Liu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b28d671 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Mutable Queue + +A [motoko] module of a mutable queue data structure supporting both `pushFront` and `pushBack` but only `popFront`. +It can be conveniently used for stable variables to hold a collection of values if you don't mind O(n) lookups. + +Internally it is just a linked list with both `first` and `last` pointers. + +You can use this library with the [vessel] package manager. + +## Development + +Documentation is non-existent but functions should be self-explanatory given their types. + +If you have installed a [nix] environment, you can run the tests like this: + +``` +nix-shell +cd test +make +``` diff --git a/package-set.dhall b/package-set.dhall new file mode 100644 index 0000000..0082e15 --- /dev/null +++ b/package-set.dhall @@ -0,0 +1 @@ +https://github.com/dfinity/vessel-package-set/releases/download/mo-0.6.18-20220107/package-set.dhall diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..16118fb --- /dev/null +++ b/shell.nix @@ -0,0 +1,8 @@ +{ pkgs ? import {} }: +let + ic-utils = import (builtins.fetchGit { + url = "https://github.com/ninegua/ic-utils"; + rev = "6e9b645e667fb59f51ad8cfa40e2fd7fdd7e52d0"; + ref = "refs/heads/main"; + }) { inherit pkgs; }; +in pkgs.mkShell { nativeBuildInputs = [ ic-utils pkgs.wasmtime ]; } diff --git a/src/Queue.mo b/src/Queue.mo new file mode 100644 index 0000000..42df7d1 --- /dev/null +++ b/src/Queue.mo @@ -0,0 +1,323 @@ +/* + * A mutable queue with pushFront and pushBack, but only popFront. + * + */ +import Iter "mo:base/Iter"; +import Nat64 "mo:base/Nat64"; +import Option "mo:base/Option"; + +import Matchers "mo:matchers/Matchers"; +import T "mo:matchers/Testable"; +import Suite "mo:matchers/Suite"; + +module Queue { + public type List = { + item: T; + var next: ?List; + }; + + public type Queue = { + var size: Nat64; + var first: ?List; + var last: ?List; + }; + + public func empty() : Queue { + { var size = 0; var first = null; var last = null; } + }; + + public func pushBack(q: Queue, item: T) : Queue { + let list : List = { item = item; var next = null }; + switch (q.first, q.last) { + case (?first, ?last) { + last.next := ?list; + q.last := last.next; + }; + case (_, _) { + q.first := ?list; + q.last := q.first; + } + }; + q.size := q.size + 1; + q + }; + + public func pushFront(item: T, q: Queue) : Queue { + let list : List = { item = item; var next = null }; + switch (q.first, q.last) { + case (?first, ?last) { + list.next := ?first; + q.first := ?list; + }; + case (_, _) { + q.first := ?list; + q.last := q.first; + } + }; + q.size := q.size + 1; + q + }; + + public func popFront(q: Queue) : ?T { + switch (q.first, q.last) { + case (?first, ?last) { + q.size := q.size - 1; + let item = first.item; + if (q.size == 0) { + q.first := null; + q.last := null; + } else { + q.first := first.next; + }; + ?item + }; + case (_, _) null; + } + }; + + public func rotate(q: Queue) : Queue { + switch (q.first, q.last) { + case (?first, ?last) { + if (q.size > 1) { + q.first := first.next; + first.next := null; + last.next := ?first; + q.last := last.next; + } + }; + case (_, _) (); + }; + q + }; + + public func remove(q: Queue, eq: T -> Bool) : Queue { + ignore removeOne(q, eq); + q + }; + + public func removeOne(q: Queue, eq: T -> Bool) : ?T { + switch (q.first, q.last) { + case (?first, ?last) { + if (eq(first.item)) { + q.first := first.next; + if (Option.isNull(q.first)) { + q.last := null; + }; + q.size := q.size - 1; + ?first.item; + } else { + var prev = first; + label L loop { + switch (prev.next) { + case null { break L }; + case (?next) { + if (eq(next.item)) { + prev.next := next.next; + if (Option.isNull(prev.next)) { + q.last := ?prev; + }; + q.size := q.size - 1; + return ?next.item; + }; + prev := next; + } + } + }; + null + } + }; + case _ { null }; + } + }; + + public func first(q: Queue) : ?T { + Option.map(q.first, func (x: List) : T { x.item }) + }; + + public func last(q: Queue) : ?T { + Option.map(q.last, func (x: List) : T { x.item }) + }; + + public func make(item: T) : Queue { + let q = empty(); + pushBack(q, item) + }; + + public func size(q: Queue) : Nat { + Nat64.toNat(q.size) + }; + + public func toIter(q: Queue) : Iter.Iter { + var cursor = q.first; + func next() : ?T { + switch (cursor) { + case null null; + case (?list) { + let item = list.item; + cursor := list.next; + ?item + } + } + }; + { next = next } + }; + + public func find(q: Queue, f: T -> Bool) : ?T { + for (item in toIter(q)) { + if (f(item)) { return ?item } + }; + null + }; + + public func fold(q: Queue, init: V, acc: (V, T) -> V) : V { + var sum = init; + for (item in toIter(q)) { + sum := acc(sum, item); + }; + sum + }; + + public func fromIter(iter: Iter.Iter) : Queue { + let q = empty(); + for (item in iter) { + ignore pushBack(q, item); + }; + q + }; + + public func fromArray(arr: [T]) : Queue { + fromIter(Iter.fromArray(arr)) + }; + + public func toArray(q: Queue) : [T] { + Iter.toArray(toIter(q)) + }; + + public func map(inp: Queue, f: A -> B) : Queue { + let out = Queue.empty(); + for (x in toIter(inp)) { + ignore pushBack(out, f(x)); + }; + out + }; + + type Suite = Suite.Suite; + public func test() : Suite { + func wellformed(q: Queue) : Queue { + switch (q.first, q.last) { + case (?first, ?last) { + assert(size(q) > 0); + if (size(q) == 1) { + assert(Option.isNull(first.next)); + assert(Option.isNull(last.next)); + } else { + var n = 0; + var p = first; + label L loop { + n := n + 1; + switch (p.next) { + case null { break L }; + case (?next) { p := next; } + } + }; + assert(n == size(q)); + } + }; + case (?_, _) { assert(false) }; + case (_, ?_) { assert(false) }; + case (_, _) { assert(size(q) == 0) }; + }; + q + }; + + func toArray_(q: Queue) : [A] { + toArray(wellformed(q)) + }; + + let to_array_test = + Suite.suite("toArray", [ + Suite.test("empty", + toArray_(empty()), + Matchers.equals(T.array(T.natTestable, []))), + Suite.test("singleton", + toArray_(make(1)), + Matchers.equals(T.array(T.natTestable, [1]))), + Suite.test("multiple", + toArray_(pushFront(0, pushBack(make(1), 2))), + Matchers.equals(T.array(T.natTestable, [0, 1, 2]))), + ]); + + let size_test = + Suite.suite("size", [ + Suite.test("empty", + size(empty()), + Matchers.equals(T.nat(0))), + Suite.test("singleton", + size(make(1)), + Matchers.equals(T.nat(1))), + Suite.test("multiple", + size(pushFront(0, pushBack(make(1), 2))), + Matchers.equals(T.nat(3))), + Suite.test("fromArray", + size(fromArray([1,2,3,4])), + Matchers.equals(T.nat(4))), + Suite.test("popFront", + size(do { let q = make(1); ignore popFront(q); q }), + Matchers.equals(T.nat(0))), + Suite.test("pushFront", + size(pushFront(0, make(1))), + Matchers.equals(T.nat(2))), + Suite.test("pushBack", + size(pushBack(make(1), 2)), + Matchers.equals(T.nat(2))), + Suite.test("rotate", + size(rotate(fromArray([1,2,3,4]))), + Matchers.equals(T.nat(4))), + ]); + + func eq(a: Nat) : Nat -> Bool { func (b: Nat) : Bool { a == b } }; + let removal_test = + Suite.suite("removal", [ + Suite.test("removeOne/empty", + removeOne(empty(), eq(10)), + Matchers.equals(T.optional(T.natTestable, null))), + Suite.test("removeOne/one", + removeOne(make(10), eq(10)), + Matchers.equals(T.optional(T.natTestable, ?10))), + Suite.test("removeOne/one_1", + removeOne(make(0), eq(10)), + Matchers.equals(T.optional(T.natTestable, null))), + Suite.test("removeOne/two", + removeOne(fromArray([3,10]), eq(3)), + Matchers.equals(T.optional(T.natTestable, ?3))), + Suite.test("removeOne/two_1", + removeOne(fromArray([3,10]), eq(10)), + Matchers.equals(T.optional(T.natTestable, ?10))), + Suite.test("removeOne/two_2", + removeOne(fromArray([3,10]), eq(0)), + Matchers.equals(T.optional(T.natTestable, null))), + Suite.test("remove/empty", + toArray_(remove(empty(), eq(10))), + Matchers.equals(T.array(T.natTestable, []))), + Suite.test("remove/one", + toArray_(remove(make(10), eq(10))), + Matchers.equals(T.array(T.natTestable, []))), + Suite.test("remove/one_1", + toArray_(remove(make(0), eq(10))), + Matchers.equals(T.array(T.natTestable, [0]))), + Suite.test("remove/two", + toArray_(remove(fromArray([3,10]), eq(3))), + Matchers.equals(T.array(T.natTestable, [10]))), + Suite.test("remove/two_1", + toArray_(remove(fromArray([3,10]), eq(10))), + Matchers.equals(T.array(T.natTestable, [3]))), + Suite.test("remove/two_2", + toArray_(remove(fromArray([3,10]), eq(0))), + Matchers.equals(T.array(T.natTestable, [3, 10]))), + ]); + + Suite.suite("Queue", [ to_array_test, size_test, removal_test ]); + } + +} + diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000..5e34c7f --- /dev/null +++ b/test/Makefile @@ -0,0 +1,20 @@ +TESTS=Queue +TARGETS=$(TESTS:%=run/%) +OBJS=$(TESTS:%=dist/%.wasm) + +all: $(TARGETS) + +run/%: dist/%.wasm + wasmtime $< + +dist: + @mkdir -p $@ + +dist/%.wasm: %.mo ../src/%.mo | dist + moc $$(vessel sources) -wasi-system-api -o $@ $< + +clean: + rm -rf dist + +.PHONY: all clean +.PRECIOUS: ${OBJS} diff --git a/test/Queue.mo b/test/Queue.mo new file mode 100644 index 0000000..0753306 --- /dev/null +++ b/test/Queue.mo @@ -0,0 +1,5 @@ +import Queue "../src/Queue"; + +import Suite "mo:matchers/Suite"; + +Suite.run(Queue.test()) diff --git a/vessel.dhall b/vessel.dhall new file mode 100644 index 0000000..19be4cd --- /dev/null +++ b/vessel.dhall @@ -0,0 +1,4 @@ +{ + dependencies = [ "base", "matchers" ], + compiler = None Text +}