diff --git a/src/Check.mo b/src/Check.mo new file mode 100644 index 0000000..6d10293 --- /dev/null +++ b/src/Check.mo @@ -0,0 +1,202 @@ +/// Module that can be used to test whether or not a BTree is valid + +import Iter "mo:base/Iter"; +import O "mo:base/Order"; +import Option "mo:base/Option"; +import Result "mo:base/Result"; + +import BT "./BTree"; + + +module { + /// Checks a BTree for validity, checking for both key ordering and node height/depth equivalence + public func check(t: BT.BTree, compare: (K, K) -> O.Order): Bool { + switch(checkTreeDepthIsValid(t)) { + case (#err) { return false }; + case _ {} + }; + + switch(checkDataOrderIsValid(t, compare)) { + case (#err) { false }; + case _ { true } + } + }; + + public type CheckDepthResult = { + #ok: Nat; // depth up to that point + #err; + }; + + // Ensures that the Btree is balanced and all sibling/cousin nodes (at same level) have the same height + public func checkTreeDepthIsValid(t: BT.BTree): CheckDepthResult { + depthCheckerHelper(t.root) + }; + + func depthCheckerHelper(node: BT.Node): CheckDepthResult { + switch(node) { + case (#leaf(_)) { #ok(1) }; + case (#internal(internalNode)) { + var depth = 1; + + var i = 0; + while (i < internalNode.children.size()) { + if (i == 0) { + switch(internalNode.children[i]) { + case null {}; + case (?n) { switch(depthCheckerHelper(n)) { + case (#err) { return #err }; + case (#ok(d)) { depth += d; }; + }} + } + } else { + switch(internalNode.children[i]) { + case null {}; + case (?n) { switch(depthCheckerHelper(n)) { + case (#err) { return #err }; + case (#ok(d)) { + if (d + 1 != depth) { return #err } + }; + }} + } + }; + + i += 1; + }; + + #ok(depth) + } + } + }; + + + public type CheckOrderResult = { + #ok; + #err; + }; + + /// Ensures the ordering of all elements in the BTree is valid + public func checkDataOrderIsValid(t : BT.BTree, compare: (K, K) -> O.Order): CheckOrderResult { + // allow for empty root (valid) + switch(t.root) { + case (#leaf(leafNode)) { + if (Option.isNull(leafNode.data.kvs[0])) { + assert leafNode.data.count == 0 + } + }; + case _ {} + }; + + rec(t.root, t.order, infCompare(compare), #infmin, #infmax) + }; + + func rec(node : BT.Node, order: Nat, compare : InfCompare, lower : Inf, upper : Inf): CheckOrderResult { + switch (node) { + case (#leaf(leafNode)) { checkData(leafNode.data, order, compare, lower, upper) }; + case (#internal(internalNode)) { checkInternal(internalNode, order, compare, lower, upper) }; + } + }; + + func checkData(data : BT.Data, order: Nat, compare : InfCompare, lower : Inf, upper : Inf): CheckOrderResult { + let expectedMaxKeys: Nat = order - 1; + if (data.kvs.size() != expectedMaxKeys) { return #err }; + + var prevKey : ?Inf = ?#infmin; + for (el in data.kvs.vals()) { + switch(el, prevKey) { + case (null, _) { + prevKey := null + }; + case (?(k, _), null) { + return #err; + }; + case (?(k, _), ?(pk)) { + if ( + compare.compare(pk, #finite k) == #less + and + compare.compare(lower, #finite k) == #less + and + compare.compare(#finite k, upper) == #less + ) { + prevKey := ?#finite k; + } else { return #err }; + } + }; + }; + + #ok + }; + + func checkInternal(internal : BT.Internal, order: Nat, compare: InfCompare, lower : Inf, upper : Inf): CheckOrderResult { + if ( + internal.children.size() != order + or + internal.children.size() != internal.data.kvs.size() + 1 + ) { return #err }; + + switch(checkData(internal.data, order, compare, lower, upper)) { + case (#err) { return #err }; + case _ {} + }; + + for (j in Iter.range(0, internal.data.kvs.size())) { + // determine lower bound for internal.children[j] + let lower_ = + // if first element, take parent context lower bound + if (j == 0) { lower } + // otherwise compare against the previous element + else { + switch(internal.data.kvs[j-1]) { + case null { return #err }; //assert false; loop {} }; // trap if the previous element is null + case (?(prevKey, _)) { #finite prevKey }; + } + }; + + // determine upper bound for internal.children[j] + let upper_ = + // if last element, take the parent context upper bound + if (j == internal.data.kvs.size()) { upper } + else { + switch(internal.data.kvs[j]) { + // if null, take the parent context upper bound. will then short circuit return at end of this function + case null { upper }; + case (?(key, _)) { #finite key } + } + }; + + switch(internal.children[j]) { + case null { return #err }; //assert false }; + case (?child) { + // recurse on the child + switch(rec(child, order, compare, lower_, upper)) { + case (#err) { return #err }; + case _ {}; + } + } + }; + + if (j + 1 >= internal.children.size() or Option.isNull(internal.children[j+1])) { return #ok }; + }; + + #ok; + }; + + + type Inf = {#infmax; #infmin; #finite : K }; + + type InfCompare = { + compare : (Inf, Inf) -> O.Order; + }; + + func infCompare(compare: (K, K) -> O.Order) : InfCompare = { + compare = func (k1 : Inf, k2 : Inf) : O.Order { + switch (k1, k2) { + case (#infmin, _) #less; + case (_, #infmin) { /* nonsense case. */ assert false; loop { } }; + case (_, #infmax) #less; + case (#infmax, _) { /* nonsense case. */ assert false; loop { } }; + case (#finite(k1), #finite(k2)) compare(k1, k2); + } + } + }; + +} \ No newline at end of file diff --git a/test/CheckTest.mo b/test/CheckTest.mo new file mode 100644 index 0000000..c214bd5 --- /dev/null +++ b/test/CheckTest.mo @@ -0,0 +1,443 @@ +import M "mo:matchers/Matchers"; +import S "mo:matchers/Suite"; +import T "mo:matchers/Testable"; + +import Nat "mo:base/Nat"; + +import Check "../src/Check"; +import BT "../src/BTree"; + +let orderResultTestableItem = func(result: Check.CheckOrderResult): T.TestableItem = { + display = func(r: Check.CheckOrderResult): Text = switch(r) { + case (#ok) "#ok"; + case (#err) "#err"; + }; + equals = func(r1: Check.CheckOrderResult, r2: Check.CheckOrderResult): Bool = switch(r1, r2) { + case (#ok, #ok) { true }; + case (#err, #err) { true }; + case _ false; + }; + item = result; +}; + +let depthResultTestableItem = func(result: Check.CheckDepthResult): T.TestableItem = { + display = func(r: Check.CheckDepthResult): Text = switch(r) { + case (#ok(depth)) "#ok: " # Nat.toText(depth); + case (#err) "#err"; + }; + equals = func(r1: Check.CheckDepthResult, r2: Check.CheckDepthResult): Bool = switch(r1, r2) { + case (#ok(d1), #ok(d2)) { d1 == d2 }; + case (#err, #err) { true }; + case _ false; + }; + item = result; +}; + + +let checkTreeDepthIsValidSuite = S.suite("checkTreeDepthIsValid", [ + S.suite("order 4 BTree", [ + S.test("test empty BTree has height 1", + Check.checkTreeDepthIsValid(BT.init(4)), + M.equals(depthResultTestableItem(#ok(1))) + ), + S.test("test 5000 auto incrementing inserts into the BTree", + do { + var i = 0; + let t = BT.init(4); + while (i < 5000) { + ignore BT.insert(t, Nat.compare, i, i); + i += 1 + }; + Check.checkTreeDepthIsValid(t); + }, + M.equals(depthResultTestableItem(#ok(8))) + ), + S.test("test 10000 auto incrementing inserts into the BTree", + do { + var i = 0; + let t = BT.init(4); + while (i < 20000) { + ignore BT.insert(t, Nat.compare, i, i); + i += 1 + }; + Check.checkTreeDepthIsValid(t); + }, + M.equals(depthResultTestableItem(#ok(9))) + ), + ]), + S.suite("order 128 BTree", [ + S.test("test empty BTree has height 1", + Check.checkTreeDepthIsValid(BT.init(128)), + M.equals(depthResultTestableItem(#ok(1))) + ), + S.test("test 5000 auto incrementing inserts into the BTree", + do { + var i = 0; + let t = BT.init(128); + while (i < 5000) { + ignore BT.insert(t, Nat.compare, i, i); + i += 1 + }; + Check.checkTreeDepthIsValid(t); + }, + M.equals(depthResultTestableItem(#ok(2))) + ), + S.test("test 20000 auto incrementing inserts into the BTree", + do { + var i = 0; + let t = BT.init(128); + while (i < 20000) { + ignore BT.insert(t, Nat.compare, i, i); + i += 1 + }; + Check.checkTreeDepthIsValid(t); + }, + M.equals(depthResultTestableItem(#ok(3))) + ), + ]), + S.test("uneven & invalid BTree depth", + do { + let t: BT.BTree = { + var root = #internal({ + data = { + kvs = [var ?(6, 6), ?(15, 15), null]; + var count = 1; + }; + children = [var + ?#leaf({ + data = { + kvs = [var ?(2, 2), ?(3, 3), ?(4, 4)]; + var count = 3; + }; + }), + ?#leaf({ + data = { + kvs = [var ?(5, 5), ?(8, 8), null]; + var count = 1; + }; + }), + ?#internal({ + data = { + kvs = [var ?(20, 20), null, null]; + var count = 1; + }; + children = [var + ?#leaf({ + data = { + kvs = [var ?(17, 17), ?(19, 19), null]; + var count = 2; + }; + }), + ?#leaf({ + data = { + kvs = [var ?(25, 25), null, null]; + var count = 1; + }; + }), + null, + null + ] + }), + null, + ] + }); + order = 4; + }; + Check.checkTreeDepthIsValid(t); + }, + M.equals(depthResultTestableItem(#err)) + ) +]); + +let checkDataOrderIsValidSuite = S.suite("checkDataDepthIsValid", [ + S.suite("order 4 BTree", [ + S.test("test empty BTree", + Check.checkDataOrderIsValid(BT.init(4), Nat.compare), + M.equals(orderResultTestableItem(#ok)) + ), + S.test("test 5000 auto incrementing inserts into the BTree", + do { + var i = 0; + let t = BT.init(4); + while (i < 5000) { + ignore BT.insert(t, Nat.compare, i, i); + i += 1 + }; + Check.checkDataOrderIsValid(t, Nat.compare); + }, + M.equals(orderResultTestableItem((#ok))) + ), + ]), + S.suite("order 16 BTrees", [ + S.test("test empty BTree", + Check.checkDataOrderIsValid(BT.init(16), Nat.compare), + M.equals(orderResultTestableItem(#ok)) + ), + S.test("test 5000 auto incrementing inserts into the BTree", + do { + var i = 0; + let t = BT.init(16); + while (i < 5000) { + ignore BT.insert(t, Nat.compare, i, i); + i += 1 + }; + Check.checkDataOrderIsValid(t, Nat.compare); + }, + M.equals(orderResultTestableItem((#ok))) + ), + ]), + S.suite("order 99 BTrees", [ + S.test("test empty BTree", + Check.checkDataOrderIsValid(BT.init(99), Nat.compare), + M.equals(orderResultTestableItem(#ok)) + ), + S.test("test 5000 auto incrementing inserts into the BTree", + do { + var i = 0; + let t = BT.init(99); + while (i < 5000) { + ignore BT.insert(t, Nat.compare, i, i); + i += 1 + }; + Check.checkDataOrderIsValid(t, Nat.compare); + }, + M.equals(orderResultTestableItem((#ok))) + ), + ]), + S.suite("are not valid btrees", [ + S.test("if have invalid nested leaf data order", + do { + let t: BT.BTree = { + var root = #internal({ + data = { + kvs = [var ?(6, 6), null, null]; + var count = 1; + }; + children = [var + ?#leaf({ + data = { + kvs = [var ?(2, 2), ?(3, 3), ?(4, 4)]; + var count = 3; + }; + }), + ?#leaf({ + data = { + kvs = [var ?(5, 5), ?(8, 8), null]; + var count = 1; + }; + }), + null, + null, + ] + }); + order = 4; + }; + Check.checkDataOrderIsValid(t, Nat.compare); + }, + M.equals(orderResultTestableItem((#err))) + ), + S.test("if have invalid internal node data order", + do { + let t: BT.BTree = { + var root = #internal({ + data = { + kvs = [var ?(6, 6), ?(0, 0), null]; + var count = 1; + }; + children = [var + ?#leaf({ + data = { + kvs = [var ?(2, 2), ?(3, 3), ?(4, 4)]; + var count = 3; + }; + }), + ?#leaf({ + data = { + kvs = [var ?(8, 8), null, null]; + var count = 1; + }; + }), + null, + null, + ] + }); + order = 4; + }; + Check.checkDataOrderIsValid(t, Nat.compare); + }, + M.equals(orderResultTestableItem(#err)) + ), + S.test("if have a null before a non-null key-value pair in a leaf", + do { + let t: BT.BTree = { + var root = #internal({ + data = { + kvs = [var ?(6, 6), null, null]; + var count = 1; + }; + children = [var + ?#leaf({ + data = { + kvs = [var ?(2, 2), ?(3, 3), ?(4, 4)]; + var count = 3; + }; + }), + ?#leaf({ + data = { + kvs = [var null, ?(8, 8), null]; + var count = 1; + }; + }), + null, + null, + ] + }); + order = 4; + }; + Check.checkDataOrderIsValid(t, Nat.compare); + }, + M.equals(orderResultTestableItem(#err)) + ), + S.test("if have a null before a non-null key-value pair in an internal", + do { + let t: BT.BTree = { + var root = #internal({ + data = { + kvs = [var null, ?(6, 6), null]; + var count = 1; + }; + children = [var + ?#leaf({ + data = { + kvs = [var ?(2, 2), ?(3, 3), ?(4, 4)]; + var count = 3; + }; + }), + ?#leaf({ + data = { + kvs = [var ?(8, 8), null, null]; + var count = 1; + }; + }), + null, + null, + ] + }); + order = 4; + }; + Check.checkDataOrderIsValid(t, Nat.compare); + }, + M.equals(orderResultTestableItem(#err)) + ), + S.test("if invalid number of children", + do { + let t: BT.BTree = { + var root = #internal({ + data = { + kvs = [var ?(6, 6), ?(0, 0), null]; + var count = 1; + }; + children = [var + ?#leaf({ + data = { + kvs = [var ?(2, 2), ?(3, 3), ?(4, 4)]; + var count = 3; + }; + }), + ?#leaf({ + data = { + kvs = [var ?(8, 8), null, null]; + var count = 1; + }; + }), + null, + null, + null + ] + }); + order = 4; + }; + Check.checkDataOrderIsValid(t, Nat.compare); + }, + M.equals(orderResultTestableItem(#err)) + ), + S.test("if invalid number of keys", + do { + let t: BT.BTree = { + var root = #internal({ + data = { + kvs = [var ?(6, 6), ?(0, 0), null]; + var count = 1; + }; + children = [var + ?#leaf({ + data = { + kvs = [var ?(2, 2), ?(3, 3), ?(4, 4), ?(5, 5)]; + var count = 3; + }; + }), + ?#leaf({ + data = { + kvs = [var ?(8, 8), null, null]; + var count = 1; + }; + }), + null, + null, + ] + }); + order = 4; + }; + Check.checkDataOrderIsValid(t, Nat.compare); + }, + M.equals(orderResultTestableItem(#err)) + ), + ]), +]); + +let checkSuite = S.suite("checkSuite", [ + S.suite("order 4 BTree", [ + S.test("test empty BTree", + Check.check(BT.init(4), Nat.compare), + M.equals(T.bool(true)) + ), + S.test("test 5000 auto incrementing inserts into the BTree", + do { + var i = 0; + let t = BT.init(4); + while (i < 5000) { + ignore BT.insert(t, Nat.compare, i, i); + i += 1 + }; + Check.check(t, Nat.compare); + }, + M.equals(T.bool(true)) + ), + ]), + S.suite("order 128 BTree", [ + S.test("test empty BTree", + Check.check(BT.init(128), Nat.compare), + M.equals(T.bool(true)) + ), + S.test("test 5000 auto incrementing inserts into the BTree", + do { + var i = 0; + let t = BT.init(128); + while (i < 5000) { + ignore BT.insert(t, Nat.compare, i, i); + i += 1 + }; + Check.check(t, Nat.compare); + }, + M.equals(T.bool(true)) + ), + ]) +]); + +S.run(S.suite("check", [ + checkTreeDepthIsValidSuite, + checkDataOrderIsValidSuite, + checkSuite, +])) + +