diff --git a/src/StableBTree/allocator.mo b/src/StableBTree/allocator.mo new file mode 100644 index 00000000..987e2dd1 --- /dev/null +++ b/src/StableBTree/allocator.mo @@ -0,0 +1,316 @@ +import Types "types"; +import Conversion "conversion"; +import Constants "constants"; +import Memory "memory"; + +import Blob "mo:base/Blob"; +import Text "mo:base/Text"; +import Debug "mo:base/Debug"; + +module { + + // For convenience: from types module + type Address = Types.Address; + type Bytes = Types.Bytes; + type Memory = Types.Memory; + + let ALLOCATOR_LAYOUT_VERSION: Nat8 = 1; + let CHUNK_LAYOUT_VERSION: Nat8 = 1; + + let ALLOCATOR_MAGIC = "BTA"; + let CHUNK_MAGIC = "CHK"; + + /// Initialize an allocator and store it in address `addr`. + /// + /// The allocator assumes that all memory from `addr` onwards is free. + /// + /// When initialized, the allocator has the following memory layout: + /// + /// [ AllocatorHeader | ChunkHeader ] + /// .. free_list_head ↑ next + /// |__________| |____ NULL + /// + public func initAllocator(memory: Memory, addr: Address, allocation_size: Bytes) : Allocator { + let free_list_head = addr + SIZE_ALLOCATOR_HEADER; + + // Create the initial memory chunk and save it directly after the allocator's header. + let chunk_header = initChunkHeader(); + saveChunkHeader(chunk_header, free_list_head, memory); + + let allocator = Allocator({ + header_addr = addr; + allocation_size; + num_allocated_chunks: Nat64 = 0; + free_list_head; + memory = memory; + }); + + allocator.saveAllocator(); + + allocator; + }; + + /// Load an allocator from memory at the given `addr`. + public func loadAllocator(memory: Memory, addr: Address) : Allocator { + + let header = { + magic = Memory.read(memory, addr, 3); + version = Memory.read(memory, addr + 3, 1)[0]; + _alignment = Memory.read(memory, addr + 3 + 1, 4); + allocation_size = Conversion.bytesToNat64(Memory.read(memory, addr + 3 + 1 + 4, 8)); + num_allocated_chunks = Conversion.bytesToNat64(Memory.read(memory, addr + 3 + 1 + 4 + 8, 8)); + free_list_head = Conversion.bytesToNat64(Memory.read(memory, addr + 3 + 1 + 4 + 8 + 8, 8)); + _buffer = Memory.read(memory, addr + 3 + 1 + 4 + 8 + 8 + 8, 16); + }; + + if (header.magic != Blob.toArray(Text.encodeUtf8(ALLOCATOR_MAGIC))) { Debug.trap("Bad magic."); }; + if (header.version != ALLOCATOR_LAYOUT_VERSION) { Debug.trap("Unsupported version."); }; + + Allocator({ + header_addr = addr; + allocation_size = header.allocation_size; + num_allocated_chunks = header.num_allocated_chunks; + free_list_head = header.free_list_head; + memory = memory; + }); + }; + + type AllocatorVariables = { + header_addr: Address; + allocation_size: Bytes; + num_allocated_chunks: Nat64; + free_list_head: Address; + memory: Memory; + }; + + /// A free list constant-size chunk allocator. + /// + /// The allocator allocates chunks of size `allocation_size` from the given `memory`. + /// + /// # Properties + /// + /// * The allocator tries to minimize its memory footprint, growing the memory in + /// size only when all the available memory is allocated. + /// + /// * The allocator makes no assumptions on the size of the memory and will + /// continue growing so long as the provided `memory` allows it. + /// + /// The allocator divides the memory into "chunks" of equal size. Each chunk contains: + /// a) A `ChunkHeader` with metadata about the chunk. + /// b) A blob of length `allocation_size` that can be used freely by the user. + /// + /// # Assumptions: + /// + /// * The given memory is not being used by any other data structure. + public class Allocator(variables: AllocatorVariables) { + + /// Members + // The address in memory where the `AllocatorHeader` is stored. + let header_addr_ : Address = variables.header_addr; + // The size of the chunk to allocate in bytes. + let allocation_size_ : Bytes = variables.allocation_size; + // The number of chunks currently allocated. + var num_allocated_chunks_ : Nat64 = variables.num_allocated_chunks; + // A linked list of unallocated chunks. + var free_list_head_ : Address = variables.free_list_head; + /// The memory to save and load the data. + let memory_ : Memory = variables.memory; + + /// Getters + public func getHeaderAddr() : Address { header_addr_; }; + public func getAllocationSize() : Bytes { allocation_size_; }; + public func getNumAllocatedChunks() : Nat64 { num_allocated_chunks_; }; + public func getFreeListHead() : Address { free_list_head_; }; + public func getMemory() : Memory { memory_; }; + + /// Allocates a new chunk from memory with size `allocation_size`. + /// + /// Internally, there are two cases: + /// + /// 1) The list of free chunks (`free_list_head`) has only one element. + /// This case happens when we initialize a new allocator, or when + /// all of the previously allocated chunks are still in use. + /// + /// Example memory layout: + /// + /// [ AllocatorHeader | ChunkHeader ] + /// .. free_list_head ↑ next + /// |__________↑ |____ NULL + /// + /// In this case, the chunk in the free list is allocated to the user + /// and a new `ChunkHeader` is appended to the allocator's memory, + /// growing the memory if necessary. + /// + /// [ AllocatorHeader | ChunkHeader | ... | ChunkHeader2 ] + /// .. free_list_head (allocated) ↑ next + /// |______________________________↑ |____ NULL + /// + /// 2) The list of free chunks (`free_list_head`) has more than one element. + /// + /// Example memory layout: + /// + /// [ AllocatorHeader | ChunkHeader1 | ... | ChunkHeader2 ] + /// .. free_list_head ↑ next ↑ next + /// |__________↑ |___________↑ |____ NULL + /// + /// In this case, the first chunk in the free list is allocated to the + /// user, and the head of the list is updated to point to the next free + /// block. + /// + /// [ AllocatorHeader | ChunkHeader1 | ... | ChunkHeader2 ] + /// .. free_list_head (allocated) ↑ next + /// |_______________________________↑ |____ NULL + /// + public func allocate() : Address { + // Get the next available chunk. + let chunk_addr = free_list_head_; + let chunk = loadChunkHeader(chunk_addr, memory_); + + // The available chunk must not be allocated. + if (chunk.allocated) { Debug.trap("Attempting to allocate an already allocated chunk."); }; + + // Allocate the chunk. + let updated_chunk = { + magic = chunk.magic; + version = chunk.version; + allocated = true; + _alignment = chunk._alignment; + next = chunk.next; + }; + saveChunkHeader(updated_chunk, chunk_addr, memory_); + + // Update the head of the free list. + if (chunk.next != Constants.NULL) { + // The next chunk becomes the new head of the list. + free_list_head_ := chunk.next; + } else { + // There is no next chunk. Shift everything by chunk size. + free_list_head_ += chunkSize(); + // Write new chunk to that location. + saveChunkHeader(initChunkHeader(), free_list_head_, memory_); + }; + + num_allocated_chunks_ += 1; + saveAllocator(); + + // Return the chunk's address offset by the chunk's header. + chunk_addr + SIZE_CHUNK_HEADER; + }; + + /// Deallocates a previously allocated chunk. + public func deallocate(address: Address) { + let chunk_addr = address - SIZE_CHUNK_HEADER; + let chunk = loadChunkHeader(chunk_addr, memory_); + + // The available chunk must be allocated. + if (not chunk.allocated) { Debug.trap("Attempting to deallocate a chunk that is not allocated."); }; + + // Deallocate the chunk. + let updated_chunk = { + magic = chunk.magic; + version = chunk.version; + allocated = false; + _alignment = chunk._alignment; + next = free_list_head_; + }; + saveChunkHeader(updated_chunk, chunk_addr, memory_); + + free_list_head_ := chunk_addr; + num_allocated_chunks_ -= 1; + + saveAllocator(); + }; + + /// Saves the allocator to memory. + public func saveAllocator() { + let header = getHeader(); + let addr = header_addr_; + + Memory.write(memory_, addr, header.magic); + Memory.write(memory_, addr + 3, [header.version]); + Memory.write(memory_, addr + 3 + 1, header._alignment); + Memory.write(memory_, addr + 3 + 1 + 4, Conversion.nat64ToBytes(header.allocation_size)); + Memory.write(memory_, addr + 3 + 1 + 4 + 8, Conversion.nat64ToBytes(header.num_allocated_chunks)); + Memory.write(memory_, addr + 3 + 1 + 4 + 8 + 8, Conversion.nat64ToBytes(header.free_list_head)); + Memory.write(memory_, addr + 3 + 1 + 4 + 8 + 8 + 8, header._buffer); + }; + + // The full size of a chunk, which is the size of the header + the `allocation_size` that's + // available to the user. + public func chunkSize() : Bytes { + allocation_size_ + SIZE_CHUNK_HEADER; + }; + + func getHeader() : AllocatorHeader{ + { + magic = Blob.toArray(Text.encodeUtf8(ALLOCATOR_MAGIC)); + version = ALLOCATOR_LAYOUT_VERSION; + _alignment = [0, 0, 0, 0]; + allocation_size = allocation_size_; + num_allocated_chunks = num_allocated_chunks_; + free_list_head = free_list_head_; + _buffer = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + }; + }; + + }; + + type AllocatorHeader = { + magic: [Nat8]; // 3 bytes + version: Nat8; + // Empty space to memory-align the following fields. + _alignment: [Nat8]; // 4 bytes + allocation_size: Bytes; + num_allocated_chunks: Nat64; + free_list_head: Address; + // Additional space reserved to add new fields without breaking backward-compatibility. + _buffer: [Nat8]; // 16 bytes + }; + + public let SIZE_ALLOCATOR_HEADER : Nat64 = 48; + + type ChunkHeader = { + magic: [Nat8]; // 3 bytes + version: Nat8; + allocated: Bool; + // Empty space to memory-align the following fields. + _alignment: [Nat8]; // 3 bytes + next: Address; + }; + + public let SIZE_CHUNK_HEADER : Nat64 = 16; + + // Initializes an unallocated chunk that doesn't point to another chunk. + func initChunkHeader() : ChunkHeader { + { + magic = Blob.toArray(Text.encodeUtf8(CHUNK_MAGIC)); + version = CHUNK_LAYOUT_VERSION; + allocated = false; + _alignment = [0, 0, 0]; + next = Constants.NULL; + }; + }; + + func saveChunkHeader(header: ChunkHeader, addr: Address, memory: Memory) { + Memory.write(memory, addr, header.magic); + Memory.write(memory, addr + 3, [header.version]); + Memory.write(memory, addr + 3 + 1, Conversion.boolToBytes(header.allocated)); + Memory.write(memory, addr + 3 + 1 + 1, header._alignment); + Memory.write(memory, addr + 3 + 1 + 1 + 3, Conversion.nat64ToBytes(header.next)); + }; + + public func loadChunkHeader(addr: Address, memory: Memory) : ChunkHeader { + let header = { + magic = Memory.read(memory, addr, 3); + version = Memory.read(memory, addr + 3, 1)[0]; + allocated = Conversion.bytesToBool(Memory.read(memory, addr + 3 + 1, 1)); + _alignment = Memory.read(memory, addr + 3 + 1 + 1, 3); + next = Conversion.bytesToNat64(Memory.read(memory, addr + 3 + 1 + 1 + 3, 8)); + }; + if (header.magic != Blob.toArray(Text.encodeUtf8(CHUNK_MAGIC))) { Debug.trap("Bad magic."); }; + if (header.version != CHUNK_LAYOUT_VERSION) { Debug.trap("Unsupported version."); }; + + header; + }; + +}; \ No newline at end of file diff --git a/src/StableBTree/btreemap.mo b/src/StableBTree/btreemap.mo new file mode 100644 index 00000000..60b9e675 --- /dev/null +++ b/src/StableBTree/btreemap.mo @@ -0,0 +1,988 @@ +import Types "types"; +import Allocator "allocator"; +import Conversion "conversion"; +import Node "node"; +import Constants "constants"; +import Iter "iter"; +import Utils "utils"; +import Memory "memory"; + +import Result "mo:base/Result"; +import Option "mo:base/Option"; +import Blob "mo:base/Blob"; +import Text "mo:base/Text"; +import Nat32 "mo:base/Nat32"; +import Debug "mo:base/Debug"; +import Array "mo:base/Array"; +import Nat64 "mo:base/Nat64"; +import Order "mo:base/Order"; +import Buffer "mo:base/Buffer"; +import Nat8 "mo:base/Nat8"; + +module { + + // For convenience: from base module + type Result = Result.Result; + // For convenience: from types module + type Address = Types.Address; + type Memory = Types.Memory; + type BytesConverter = Types.BytesConverter; + type InsertError = Types.InsertError; + type NodeType = Types.NodeType; + type Entry = Types.Entry; + type Cursor = Types.Cursor; + // For convenience: from node module + type Node = Node.Node; + // For convenience: from iter module + type Iter = Iter.Iter; + // For convenience: from allocator module + type Allocator = Allocator.Allocator; + + let LAYOUT_VERSION : Nat8 = 1; + let MAGIC = "BTR"; + + /// Initializes a `BTreeMap`. + /// + /// If the memory provided already contains a `BTreeMap`, then that + /// map is loaded. Otherwise, a new `BTreeMap` instance is created. + public func init( + memory : Memory, + max_key_size : Nat32, + max_value_size : Nat32, + key_converter: BytesConverter, + value_converter: BytesConverter + ) : BTreeMap { + if (memory.size() == 0) { + // Memory is empty. Create a new map. + return new(memory, max_key_size, max_value_size, key_converter, value_converter); + }; + + // Check if the magic in the memory corresponds to a BTreeMap. + let dst = Memory.read(memory, 0, 3); + if (dst != Blob.toArray(Text.encodeUtf8(MAGIC))) { + // No BTreeMap found. Create a new instance. + return new(memory, max_key_size, max_value_size, key_converter, value_converter); + }; + + // The memory already contains a BTreeMap. Load it. + return load(memory, key_converter, value_converter); + }; + + /// Creates a new instance a `BTreeMap`. + /// + /// The given `memory` is assumed to be exclusively reserved for this data + /// structure and that it starts at address zero. Typically `memory` will + /// be an instance of `RestrictedMemory`. + /// + /// When initialized, the data structure has the following memory layout: + /// + /// | BTreeHeader | Allocator | ... free memory for nodes | + /// + /// See `Allocator` for more details on its own memory layout. + public func new( + memory : Memory, + max_key_size : Nat32, + max_value_size : Nat32, + key_converter: BytesConverter, + value_converter: BytesConverter + ) : BTreeMap { + // Because we assume that we have exclusive access to the memory, + // we can store the `BTreeHeader` at address zero, and the allocator is + // stored directly after the `BTreeHeader`. + let allocator_addr = Constants.ADDRESS_0 + B_TREE_HEADER_SIZE; + let btree = BTreeMap({ + root_addr = Constants.NULL; + max_key_size = max_key_size; + max_value_size = max_value_size; + key_converter = key_converter; + value_converter = value_converter; + allocator = Allocator.initAllocator(memory, allocator_addr, Node.size(max_key_size, max_value_size)); + length : Nat64 = 0; + memory = memory; + }); + + btree.save(); + + btree; + }; + + /// Loads the map from memory. + public func load( + memory : Memory, + key_converter: BytesConverter, + value_converter: BytesConverter + ) : BTreeMap { + // Read the header from memory. + let header = loadBTreeHeader(Constants.NULL, memory); + let allocator_addr = Constants.ADDRESS_0 + B_TREE_HEADER_SIZE; + + BTreeMap({ + root_addr = header.root_addr; + max_key_size = header.max_key_size; + max_value_size = header.max_value_size; + key_converter = key_converter; + value_converter = value_converter; + allocator = Allocator.loadAllocator(memory, allocator_addr); + length = header.length; + memory = memory; + }); + }; + + let B_TREE_HEADER_SIZE : Nat64 = 52; + + type BTreeHeader = { + magic: [Nat8]; // 3 bytes + version: Nat8; + max_key_size: Nat32; + max_value_size: Nat32; + root_addr: Address; + length: Nat64; + // Additional space reserved to add new fields without breaking backward-compatibility. + _buffer: [Nat8]; // 24 bytes + }; + + func saveBTreeHeader(header: BTreeHeader, addr: Address, memory: Memory) { + Memory.write(memory, addr , header.magic); + Memory.write(memory, addr + 3 , [header.version]); + Memory.write(memory, addr + 3 + 1 , Conversion.nat32ToBytes(header.max_key_size)); + Memory.write(memory, addr + 3 + 1 + 4 , Conversion.nat32ToBytes(header.max_value_size)); + Memory.write(memory, addr + 3 + 1 + 4 + 4 , Conversion.nat64ToBytes(header.root_addr)); + Memory.write(memory, addr + 3 + 1 + 4 + 4 + 8 , Conversion.nat64ToBytes(header.length)); + Memory.write(memory, addr + 3 + 1 + 4 + 4 + 8 + 8, header._buffer); + }; + + func loadBTreeHeader(addr: Address, memory: Memory) : BTreeHeader { + let header = { + magic = Memory.read(memory, addr , 3); + version = Memory.read(memory, addr + 3 , 1)[0]; + max_key_size = Conversion.bytesToNat32(Memory.read(memory, addr + 3 + 1 , 4)); + max_value_size = Conversion.bytesToNat32(Memory.read(memory, addr + 3 + 1 + 4 , 4)); + root_addr = Conversion.bytesToNat64(Memory.read(memory, addr + 3 + 1 + 4 + 4 , 8)); + length = Conversion.bytesToNat64(Memory.read(memory, addr + 3 + 1 + 4 + 4 + 8 , 8)); + _buffer = Memory.read(memory, addr + 3 + 1 + 4 + 4 + 8 + 8, 24); + }; + if (header.magic != Blob.toArray(Text.encodeUtf8(MAGIC))) { Debug.trap("Bad magic."); }; + if (header.version != LAYOUT_VERSION) { Debug.trap("Unsupported version."); }; + + header; + }; + + type BTreeMapMembers = { + root_addr : Address; + max_key_size : Nat32; + max_value_size : Nat32; + key_converter: BytesConverter; + value_converter: BytesConverter; + allocator : Allocator; + length : Nat64; + memory : Memory; + }; + + public class BTreeMap(members: BTreeMapMembers) = self { + + /// Members + // The address of the root node. If a root node doesn't exist, the address is set to NULL. + var root_addr_ : Address = members.root_addr; + // The maximum size a key can have. + let max_key_size_ : Nat32 = members.max_key_size; + // The maximum size a value can have. + let max_value_size_ : Nat32 = members.max_value_size; + /// To convert the key into/from bytes. + let key_converter_ : BytesConverter = members.key_converter; + /// To convert the value into/from bytes. + let value_converter_ : BytesConverter = members.value_converter; + // An allocator used for managing memory and allocating nodes. + let allocator_ : Allocator = members.allocator; + // The number of elements in the map. + var length_ : Nat64 = members.length; + /// The memory used to load/save the map. + let memory_ : Memory = members.memory; + + /// Getters + public func getRootAddr() : Address { root_addr_; }; + public func getMaxKeySize() : Nat32 { max_key_size_; }; + public func getMaxValueSize() : Nat32 { max_value_size_; }; + public func getKeyConverter() : BytesConverter { key_converter_; }; + public func getValueConverter() : BytesConverter { value_converter_; }; + public func getAllocator() : Allocator { allocator_; }; + public func getLength() : Nat64 { length_; }; + public func getMemory() : Memory { memory_; }; + + /// Inserts a key-value pair into the map. + /// + /// The previous value of the key, if present, is returned. + /// + /// The size of the key/value must be <= the max key/value sizes configured + /// for the map. Otherwise, an `InsertError` is returned. + public func insert(k: K, v: V) : Result { + let key = key_converter_.toBytes(k); + let value = value_converter_.toBytes(v); + + // Verify the size of the key. + if (key.size() > Nat32.toNat(max_key_size_)) { + return #err(#KeyTooLarge { + given = key.size(); + max = Nat32.toNat(max_key_size_); + }); + }; + + // Verify the size of the value. + if (value.size() > Nat32.toNat(max_value_size_)) { + return #err(#ValueTooLarge { + given = value.size(); + max = Nat32.toNat(max_value_size_); + }); + }; + + let root = do { + if (root_addr_ == Constants.NULL) { + // No root present. Allocate one. + let node = allocateNode(#Leaf); + root_addr_ := node.getAddress(); + save(); + node; + } else { + // Load the root from memory. + var root = loadNode(root_addr_); + + // Check if the key already exists in the root. + switch(root.getKeyIdx(key)) { + case(#ok(idx)){ + // The key exists. Overwrite it and return the previous value. + let (_, previous_value) = root.swapEntry(idx, (key, value)); + root.save(memory_); + return #ok(?(value_converter_.fromBytes(previous_value))); + }; + case(#err(_)){ + // If the root is full, we need to introduce a new node as the root. + // + // NOTE: In the case where we are overwriting an existing key, then introducing + // a new root node isn't strictly necessary. However, that's a micro-optimization + // that adds more complexity than it's worth. + if (root.isFull()) { + // The root is full. Allocate a new node that will be used as the new root. + var new_root = allocateNode(#Internal); + + // The new root has the old root as its only child. + new_root.addChild(root_addr_); + + // Update the root address. + root_addr_ := new_root.getAddress(); + save(); + + // Split the old (full) root. + splitChild(new_root, 0); + + new_root; + } else { + root; + }; + }; + }; + }; + }; + #ok(Option.map<[Nat8], V>( + insertNonFull(root, key, value), + func(bytes: [Nat8]) : V { value_converter_.fromBytes(bytes); }) + ); + }; + + // Inserts an entry into a node that is *not full*. + func insertNonFull(node: Node, key: [Nat8], value: [Nat8]) : ?[Nat8] { + // We're guaranteed by the caller that the provided node is not full. + assert(not node.isFull()); + + // Look for the key in the node. + switch(node.getKeyIdx(key)){ + case(#ok(idx)){ + // The key is already in the node. + // Overwrite it and return the previous value. + let (_, previous_value) = node.swapEntry(idx, (key, value)); + + node.save(memory_); + return ?previous_value; + }; + case(#err(idx)){ + // The key isn't in the node. `idx` is where that key should be inserted. + + switch(node.getNodeType()) { + case(#Leaf){ + // The node is a non-full leaf. + // Insert the entry at the proper location. + node.insertEntry(idx, (key, value)); + + node.save(memory_); + + // Update the length. + length_ += 1; + save(); + + // No previous value to return. + return null; + }; + case(#Internal){ + // The node is an internal node. + // Load the child that we should add the entry to. + var child = loadNode(node.getChild(idx)); + + if (child.isFull()) { + // Check if the key already exists in the child. + switch(child.getKeyIdx(key)) { + case(#ok(idx)){ + // The key exists. Overwrite it and return the previous value. + let (_, previous_value) = child.swapEntry(idx, (key, value)); + + child.save(memory_); + return ?previous_value; + }; + case(#err(_)){ + // The child is full. Split the child. + splitChild(node, idx); + + // The children have now changed. Search again for + // the child where we need to store the entry in. + let index = switch(node.getKeyIdx(key)){ + case(#ok(i)) { i; }; + case(#err(i)) { i; }; + }; + + child := loadNode(node.getChild(index)); + }; + }; + }; + + // The child should now be not full. + assert(not child.isFull()); + + insertNonFull(child, key, value); + }; + }; + }; + }; + }; + + // Takes as input a nonfull internal `node` and index to its full child, then + // splits this child into two, adding an additional child to `node`. + // + // Example: + // + // [ ... M Y ... ] + // | + // [ N O P Q R S T U V W X ] + // + // + // After splitting becomes: + // + // [ ... M S Y ... ] + // / \ + // [ N O P Q R ] [ T U V W X ] + // + func splitChild(node: Node, full_child_idx: Nat) { + // The node must not be full. + assert(not node.isFull()); + + // The node's child must be full. + var full_child = loadNode(node.getChild(full_child_idx)); + assert(full_child.isFull()); + + // Create a sibling to this full child (which has to be the same type). + var sibling = allocateNode(full_child.getNodeType()); + assert(sibling.getNodeType() == full_child.getNodeType()); + + // Move the values above the median into the new sibling. + sibling.setEntries(Utils.splitOff(full_child.getEntries(), Constants.B)); + + if (full_child.getNodeType() == #Internal) { + sibling.setChildren(Utils.splitOff
(full_child.getChildren(), Constants.B)); + }; + + // Add sibling as a new child in the node. + node.insertChild(full_child_idx + 1, sibling.getAddress()); + + // Move the median entry into the node. + switch(full_child.popEntry()){ + case(null){ + Debug.trap("A full child cannot be empty"); + }; + case(?median_entry){ + node.insertEntry(full_child_idx, median_entry); + sibling.save(memory_); + full_child.save(memory_); + node.save(memory_); + }; + }; + }; + + /// Returns the value associated with the given key if it exists. + public func get(key: K) : ?V { + if (root_addr_ == Constants.NULL) { + return null; + }; + Option.map<[Nat8], V>( + getHelper(root_addr_, key_converter_.toBytes(key)), + func(bytes: [Nat8]) : V { value_converter_.fromBytes(bytes); } + ); + }; + + func getHelper(node_addr: Address, key: [Nat8]) : ?[Nat8] { + let node = loadNode(node_addr); + switch(node.getKeyIdx(key)){ + case(#ok(idx)) { ?node.getEntry(idx).1; }; + case(#err(idx)) { + switch(node.getNodeType()) { + case(#Leaf) { null; }; // Key not found. + case(#Internal) { + // The key isn't in the node. Look for the key in the child. + getHelper(node.getChild(idx), key); + }; + }; + }; + }; + }; + + /// Returns `true` if the key exists in the map, `false` otherwise. + public func containsKey(key: K) : Bool { + Option.isSome(get(key)); + }; + + /// Returns `true` if the map contains no elements. + public func isEmpty() : Bool { + (length_ == 0); + }; + + /// Removes a key from the map, returning the previous value at the key if it exists. + public func remove(key: K) : ?V { + if (root_addr_ == Constants.NULL) { + return null; + }; + Option.map<[Nat8], V>( + removeHelper(root_addr_, key_converter_.toBytes(key)), + func(bytes: [Nat8]) : V { value_converter_.fromBytes(bytes); } + ); + }; + + // A helper method for recursively removing a key from the B-tree. + func removeHelper(node_addr: Address, key: [Nat8]) : ?[Nat8] { + var node = loadNode(node_addr); + + if(node.getAddress() != root_addr_){ + // We're guaranteed that whenever this method is called the number + // of keys is >= `B`. Note that this is higher than the minimum required + // in a node, which is `B - 1`, and that's because this strengthened + // condition allows us to delete an entry in a single pass most of the + // time without having to back up. + assert(node.getEntries().size() >= Constants.B); + }; + + switch(node.getNodeType()) { + case(#Leaf) { + switch(node.getKeyIdx(key)){ + case(#ok(idx)) { + // Case 1: The node is a leaf node and the key exists in it. + // This is the simplest case. The key is removed from the leaf. + let value = node.removeEntry(idx).1; + length_ -= 1; + + if (node.getEntries().size() == 0) { + if (node.getAddress() != root_addr_) { + Debug.trap("Removal can only result in an empty leaf node if that node is the root"); + }; + + // Deallocate the empty node. + allocator_.deallocate(node.getAddress()); + root_addr_ := Constants.NULL; + } else { + node.save(memory_); + }; + + save(); + ?value; + }; + case(_) { null; }; // Key not found. + }; + }; + case(#Internal) { + switch(node.getKeyIdx(key)){ + case(#ok(idx)) { + // Case 2: The node is an internal node and the key exists in it. + + // Check if the child that precedes `key` has at least `B` keys. + let left_child = loadNode(node.getChild(idx)); + if (left_child.getEntries().size() >= Constants.B) { + // Case 2.a: The node's left child has >= `B` keys. + // + // parent + // [..., key, ...] + // / \ + // [left child] [...] + // / \ + // [...] [..., key predecessor] + // + // In this case, we replace `key` with the key's predecessor from the + // left child's subtree, then we recursively delete the key's + // predecessor for the following end result: + // + // parent + // [..., key predecessor, ...] + // / \ + // [left child] [...] + // / \ + // [...] [...] + + // Recursively delete the predecessor. + // TODO(EXC-1034): Do this in a single pass. + let predecessor = left_child.getMax(memory_); + ignore removeHelper(node.getChild(idx), predecessor.0); + + // Replace the `key` with its predecessor. + let (_, old_value) = node.swapEntry(idx, predecessor); + + // Save the parent node. + node.save(memory_); + return ?old_value; + }; + + // Check if the child that succeeds `key` has at least `B` keys. + let right_child = loadNode(node.getChild(idx + 1)); + if (right_child.getEntries().size() >= Constants.B) { + // Case 2.b: The node's right child has >= `B` keys. + // + // parent + // [..., key, ...] + // / \ + // [...] [right child] + // / \ + // [key successor, ...] [...] + // + // In this case, we replace `key` with the key's successor from the + // right child's subtree, then we recursively delete the key's + // successor for the following end result: + // + // parent + // [..., key successor, ...] + // / \ + // [...] [right child] + // / \ + // [...] [...] + + // Recursively delete the successor. + // TODO(EXC-1034): Do this in a single pass. + let successor = right_child.getMin(memory_); + ignore removeHelper(node.getChild(idx + 1), successor.0); + + // Replace the `key` with its successor. + let (_, old_value) = node.swapEntry(idx, successor); + + // Save the parent node. + node.save(memory_); + return ?old_value; + }; + + // Case 2.c: Both the left child and right child have B - 1 keys. + // + // parent + // [..., key, ...] + // / \ + // [left child] [right child] + // + // In this case, we merge (left child, key, right child) into a single + // node of size 2B - 1. The result will look like this: + // + // parent + // [... ...] + // | + // [left child, `key`, right child] <= new child + // + // We then recurse on this new child to delete `key`. + // + // If `parent` becomes empty (which can only happen if it's the root), + // then `parent` is deleted and `new_child` becomes the new root. + let num_keys : Int = Constants.B - 1; + assert(left_child.getEntries().size() == num_keys); + assert(right_child.getEntries().size() == num_keys); + + // Merge the right child into the left child. + let new_child = merge(right_child, left_child, node.removeEntry(idx)); + + // Remove the right child from the parent node. + ignore node.removeChild(idx + 1); + + if (node.getEntries().size() == 0) { + // Can only happen if this node is root. + assert(node.getAddress() == root_addr_); + assert(node.getChildren().toArray() == [new_child.getAddress()]); + + root_addr_ := new_child.getAddress(); + + // Deallocate the root node. + allocator_.deallocate(node.getAddress()); + save(); + }; + + node.save(memory_); + new_child.save(memory_); + + // Recursively delete the key. + removeHelper(new_child.getAddress(), key); + }; + case(#err(idx)) { + // Case 3: The node is an internal node and the key does NOT exist in it. + + // If the key does exist in the tree, it will exist in the subtree at index + // `idx`. + var child = loadNode(node.getChild(idx)); + + if (child.getEntries().size() >= Constants.B) { + // The child has enough nodes. Recurse to delete the `key` from the + // `child`. + return removeHelper(node.getChild(idx), key); + }; + + // The child has < `B` keys. Let's see if it has a sibling with >= `B` keys. + var left_sibling = do { + if (idx > 0) { + ?loadNode(node.getChild(idx- 1)); + } else { + null; + }; + }; + + var right_sibling = do { + if (idx + 1 < node.getChildren().size()) { + ?loadNode(node.getChild(idx + 1)); + } else { + null; + }; + }; + + switch(left_sibling){ + case(null){}; + case(?left_sibling){ + if (left_sibling.getEntries().size() >= Constants.B) { + // Case 3.a (left): The child has a left sibling with >= `B` keys. + // + // [d] (parent) + // / \ + // (left sibling) [a, b, c] [e, f] (child) + // \ + // [c'] + // + // In this case, we move a key down from the parent into the child + // and move a key from the left sibling up into the parent + // resulting in the following tree: + // + // [c] (parent) + // / \ + // (left sibling) [a, b] [d, e, f] (child) + // / + // [c'] + // + // We then recurse to delete the key from the child. + + // Remove the last entry from the left sibling. + switch(left_sibling.popEntry()){ + // We tested before that the entries size is not zero, this should never happen. + case(null) { Debug.trap("The left sibling entries must not be empty."); }; + case(?(left_sibling_key, left_sibling_value)){ + + // Replace the parent's entry with the one from the left sibling. + let (parent_key, parent_value) = node + .swapEntry(idx - 1, (left_sibling_key, left_sibling_value)); + + // Move the entry from the parent into the child. + child.insertEntry(0, (parent_key, parent_value)); + + // Move the last child from left sibling into child. + switch(left_sibling.popChild()) { + case(?last_child){ + assert(left_sibling.getNodeType() == #Internal); + assert(child.getNodeType() == #Internal); + + child.insertChild(0, last_child); + }; + case(null){ + assert(left_sibling.getNodeType() == #Leaf); + assert(child.getNodeType() == #Leaf); + }; + }; + + left_sibling.save(memory_); + child.save(memory_); + node.save(memory_); + return removeHelper(child.getAddress(), key); + }; + }; + }; + }; + }; + + switch(right_sibling){ + case(null){}; + case(?right_sibling){ + if (right_sibling.getEntries().size() >= Constants.B) { + // Case 3.a (right): The child has a right sibling with >= `B` keys. + // + // [c] (parent) + // / \ + // (child) [a, b] [d, e, f] (left sibling) + // / + // [d'] + // + // In this case, we move a key down from the parent into the child + // and move a key from the right sibling up into the parent + // resulting in the following tree: + // + // [d] (parent) + // / \ + // (child) [a, b, c] [e, f] (right sibling) + // \ + // [d'] + // + // We then recurse to delete the key from the child. + + // Remove the first entry from the right sibling. + let (right_sibling_key, right_sibling_value) = + right_sibling.removeEntry(0); + + // Replace the parent's entry with the one from the right sibling. + let parent_entry = + node.swapEntry(idx, (right_sibling_key, right_sibling_value)); + + // Move the entry from the parent into the child. + child.addEntry(parent_entry); + + // Move the first child of right_sibling into `child`. + switch(right_sibling.getNodeType()) { + case(#Internal) { + assert(child.getNodeType() == #Internal); + child.addChild(right_sibling.removeChild(0)); + }; + case(#Leaf) { + assert(child.getNodeType() == #Leaf); + }; + }; + + right_sibling.save(memory_); + child.save(memory_); + node.save(memory_); + return removeHelper(child.getAddress(), key); + }; + }; + }; + + // Case 3.b: neither siblings of the child have >= `B` keys. + + switch(left_sibling){ + case(null){}; + case(?left_sibling){ + // Merge child into left sibling if it exists. + + let left_sibling_address = left_sibling.getAddress(); + ignore merge(child, left_sibling, node.removeEntry(idx - 1)); + // Removing child from parent. + ignore node.removeChild(idx); + + if (node.getEntries().size() == 0) { + allocator_.deallocate(node.getAddress()); + + if (node.getAddress() == root_addr_) { + // Update the root. + root_addr_ := left_sibling_address; + save(); + }; + } else { + node.save(memory_); + }; + + return removeHelper(left_sibling_address, key); + }; + }; + + switch(right_sibling){ + case(null){}; + case(?right_sibling){ + // Merge child into right sibling. + + let right_sibling_address = right_sibling.getAddress(); + ignore merge(child, right_sibling, node.removeEntry(idx)); + + // Removing child from parent. + ignore node.removeChild(idx); + + if (node.getEntries().size() == 0) { + allocator_.deallocate(node.getAddress()); + + if (node.getAddress() == root_addr_) { + // Update the root. + root_addr_ := right_sibling_address; + save(); + }; + } else { + node.save(memory_); + }; + + return removeHelper(right_sibling_address, key); + }; + }; + + Debug.trap("At least one of the siblings must exist."); + }; + }; + }; + }; + }; + + // Returns an iterator over the entries of the map, sorted by key. + public func iter() : Iter { + Iter.new(self); + }; + + /// Returns an iterator over the entries in the map where keys begin with the given `prefix`. + /// If the optional `offset` is set, the iterator returned will start from the entry that + /// contains this `offset` (while still iterating over all remaining entries that begin + /// with the given `prefix`). + public func range(prefix: [Nat8], offset: ?[Nat8]) : Iter { + if (root_addr_ == Constants.NULL) { + // Map is empty. + return Iter.empty(self); + }; + + var node = loadNode(root_addr_); + let cursors : Buffer.Buffer = Buffer.Buffer(0); + + loop { + // Look for the prefix in the node. + let pivot = Utils.toBuffer(prefix); + switch(offset) { + case(null) {}; + case(?offset){ + pivot.append(Utils.toBuffer(offset)); + }; + }; + let idx = switch(node.getKeyIdx(pivot.toArray())){ + case(#err(idx)) { idx; }; + case(#ok(idx)) { idx; }; + }; + + // If `prefix` is a key in the node, then `idx` would return its + // location. Otherwise, `idx` is the location of the next key in + // lexicographical order. + + // Load the next child of the node to visit if it exists. + // This is done first to avoid cloning the node. + let child = switch(node.getNodeType()) { + case(#Internal) { + // Note that loading a child node cannot fail since + // len(children) = len(entries) + 1 + ?loadNode(node.getChild(idx)); + }; + case(#Leaf) { null; }; + }; + + // If the prefix is found in the node, then add a cursor starting from its index. + if (idx < node.getEntries().size() and Utils.isPrefixOf(prefix, node.getEntry(idx).0, Nat8.equal)){ + cursors.add(#Node { + node; + next = #Entry(Nat64.fromNat(idx)); + }); + }; + + switch(child) { + case(null) { + // Leaf node. Return an iterator with the found cursors. + switch(offset) { + case(?offset) { + return Iter.newWithPrefixAndOffset( + self, prefix, offset, cursors.toArray() + ); + }; + case(null) { + return Iter.newWithPrefix(self, prefix, cursors.toArray()); + }; + }; + }; + case(?child) { + // Iterate over the child node. + node := child; + }; + }; + }; + }; + + // Merges one node (`source`) into another (`into`), along with a median entry. + // + // Example (values are not included for brevity): + // + // Input: + // Source: [1, 2, 3] + // Into: [5, 6, 7] + // Median: 4 + // + // Output: + // [1, 2, 3, 4, 5, 6, 7] (stored in the `into` node) + // `source` is deallocated. + func merge(source: Node, into: Node, median: Entry) : Node { + assert(source.getNodeType() == into.getNodeType()); + assert(source.getEntries().size() != 0); + assert(into.getEntries().size() != 0); + + let into_address = into.getAddress(); + let source_address = source.getAddress(); + + // Figure out which node contains lower values than the other. + let (lower, higher) = do { + if (Order.isLess(Node.compareEntryKeys(source.getEntry(0).0, into.getEntry(0).0))){ + (source, into); + } else { + (into, source); + }; + }; + + lower.addEntry(median); + + lower.appendEntries(higher.getEntries()); + + lower.setAddress(into_address); + + // Move the children (if any exist). + lower.appendChildren(higher.getChildren()); + + lower.save(memory_); + + allocator_.deallocate(source_address); + lower; + }; + + func allocateNode(node_type: NodeType) : Node { + Node.Node({ + address = allocator_.allocate(); + entries = []; + children = []; + node_type; + max_key_size = max_key_size_; + max_value_size = max_value_size_; + }); + }; + + public func loadNode(address: Address) : Node { + Node.load(address, memory_, max_key_size_, max_value_size_); + }; + + // Saves the map to memory. + public func save() { + let header : BTreeHeader = { + magic = Blob.toArray(Text.encodeUtf8(MAGIC)); + version = LAYOUT_VERSION; + root_addr = root_addr_; + max_key_size = max_key_size_; + max_value_size = max_value_size_; + length = length_; + _buffer = Array.freeze(Array.init(24, 0)); + }; + + saveBTreeHeader(header, Constants.ADDRESS_0, memory_); + }; + + }; + +}; \ No newline at end of file diff --git a/src/StableBTree/constants.mo b/src/StableBTree/constants.mo new file mode 100644 index 00000000..e86bc7bc --- /dev/null +++ b/src/StableBTree/constants.mo @@ -0,0 +1,13 @@ +module { + + public let WASM_PAGE_SIZE : Nat64 = 65536; + + public let NULL : Nat64 = 0; + + public let ADDRESS_0 : Nat64 = 0; + + /// The minimum degree to use in the btree. + /// This constant is taken from Rust's std implementation of BTreeMap. + public let B : Nat = 6; + +}; \ No newline at end of file diff --git a/src/StableBTree/conversion.mo b/src/StableBTree/conversion.mo new file mode 100644 index 00000000..7c23b347 --- /dev/null +++ b/src/StableBTree/conversion.mo @@ -0,0 +1,258 @@ +import Utils "utils"; + +import Buffer "mo:base/Buffer"; +import Nat8 "mo:base/Nat8"; +import Nat16 "mo:base/Nat16"; +import Nat32 "mo:base/Nat32"; +import Nat64 "mo:base/Nat64"; +import Char "mo:base/Char"; +import Iter "mo:base/Iter"; +import Blob "mo:base/Blob"; +import Principal "mo:base/Principal"; +import List "mo:base/List"; +import Text "mo:base/Text"; +import Int "mo:base/Int"; +import Array "mo:base/Array"; + +module { + + // Comes from Candy library conversion.mo: https://raw.githubusercontent.com/skilesare/candy_library/main/src/conversion.mo + + ////////////////////////////////////////////////////////////////////// + // The following functions converst standard types to Byte arrays + // From there you can easily get to blobs if necessary with the Blob package + ////////////////////////////////////////////////////////////////////// + + public func nat64ToBytes(x : Nat64) : [Nat8] { + + [ Nat8.fromNat(Nat64.toNat((x >> 56) & (255))), + Nat8.fromNat(Nat64.toNat((x >> 48) & (255))), + Nat8.fromNat(Nat64.toNat((x >> 40) & (255))), + Nat8.fromNat(Nat64.toNat((x >> 32) & (255))), + Nat8.fromNat(Nat64.toNat((x >> 24) & (255))), + Nat8.fromNat(Nat64.toNat((x >> 16) & (255))), + Nat8.fromNat(Nat64.toNat((x >> 8) & (255))), + Nat8.fromNat(Nat64.toNat((x & 255))) ]; + }; + + public func nat32ToBytes(x : Nat32) : [Nat8] { + + [ Nat8.fromNat(Nat32.toNat((x >> 24) & (255))), + Nat8.fromNat(Nat32.toNat((x >> 16) & (255))), + Nat8.fromNat(Nat32.toNat((x >> 8) & (255))), + Nat8.fromNat(Nat32.toNat((x & 255))) ]; + }; + + /// Returns [Nat8] of size 4 of the Nat16 + public func nat16ToBytes(x : Nat16) : [Nat8] { + + [ Nat8.fromNat(Nat16.toNat((x >> 8) & (255))), + Nat8.fromNat(Nat16.toNat((x & 255))) ]; + }; + + public func bytesToNat16(bytes: [Nat8]) : Nat16{ + + (Nat16.fromNat(Nat8.toNat(bytes[0])) << 8) + + (Nat16.fromNat(Nat8.toNat(bytes[1]))); + }; + + public func bytesToNat32(bytes: [Nat8]) : Nat32{ + + (Nat32.fromNat(Nat8.toNat(bytes[0])) << 24) + + (Nat32.fromNat(Nat8.toNat(bytes[1])) << 16) + + (Nat32.fromNat(Nat8.toNat(bytes[2])) << 8) + + (Nat32.fromNat(Nat8.toNat(bytes[3]))); + }; + + public func bytesToNat64(bytes: [Nat8]) : Nat64{ + + (Nat64.fromNat(Nat8.toNat(bytes[0])) << 56) + + (Nat64.fromNat(Nat8.toNat(bytes[1])) << 48) + + (Nat64.fromNat(Nat8.toNat(bytes[2])) << 40) + + (Nat64.fromNat(Nat8.toNat(bytes[3])) << 32) + + (Nat64.fromNat(Nat8.toNat(bytes[4])) << 24) + + (Nat64.fromNat(Nat8.toNat(bytes[5])) << 16) + + (Nat64.fromNat(Nat8.toNat(bytes[6])) << 8) + + (Nat64.fromNat(Nat8.toNat(bytes[7]))); + }; + + + public func natToBytes(n : Nat) : [Nat8] { + + var a : Nat8 = 0; + var b : Nat = n; + var bytes = List.nil(); + var test = true; + while test { + a := Nat8.fromNat(b % 256); + b := b / 256; + bytes := List.push(a, bytes); + test := b > 0; + }; + List.toArray(bytes); + }; + + public func bytesToNat(bytes : [Nat8]) : Nat { + + var n : Nat = 0; + var i = 0; + Array.foldRight(bytes, (), func (byte, _) { + n += Nat8.toNat(byte) * 256 ** i; + i += 1; + return; + }); + return n; + }; + + public func textToByteBuffer(_text : Text) : Buffer.Buffer{ + + let result : Buffer.Buffer = Buffer.Buffer((_text.size() * 4) +4); + for(thisChar in _text.chars()){ + for(thisByte in nat32ToBytes(Char.toNat32(thisChar)).vals()){ + result.add(thisByte); + }; + }; + return result; + }; + + public func textToBytes(_text : Text) : [Nat8]{ + + return textToByteBuffer(_text).toArray(); + }; + + //encodes a string it to a giant int + public func encodeTextAsNat(phrase : Text) : ?Nat { + var theSum : Nat = 0; + Iter.iterate(Text.toIter(phrase), func (x : Char, n : Nat){ + //todo: check for digits + theSum := theSum + ((Nat32.toNat(Char.toNat32(x)) - 48) * 10 ** (phrase.size()-n-1)); + }); + return ?theSum; + }; + + //conversts "10" to 10 + public func textToNat( txt : Text) : ?Nat { + if(txt.size() > 0){ + let chars = txt.chars(); + var num : Nat = 0; + for (v in chars){ + let charToNum = Nat32.toNat(Char.toNat32(v)-48); + if(charToNum >= 0 and charToNum <= 9){ + num := num * 10 + charToNum; + } else { + return null; + }; + }; + ?num; + }else { + return null; + }; + }; + + public func bytesToText(_bytes : [Nat8]) : Text{ + + var result : Text = ""; + var aChar : [var Nat8] = [var 0, 0, 0, 0]; + + for(thisChar in Iter.range(0,_bytes.size())){ + if(thisChar > 0 and thisChar % 4 == 0){ + aChar[0] := _bytes[thisChar-4]; + aChar[1] := _bytes[thisChar-3]; + aChar[2] := _bytes[thisChar-2]; + aChar[3] := _bytes[thisChar-1]; + result := result # Char.toText(Char.fromNat32(bytesToNat32(Array.freeze(aChar)))); + }; + }; + return result; + }; + + public func principalToBytes(_principal: Principal) : [Nat8]{ + + return Blob.toArray(Principal.toBlob(_principal)); + }; + + public func bytesToPrincipal(_bytes: [Nat8]) : Principal{ + + return Principal.fromBlob(Blob.fromArray(_bytes)); + }; + + public func boolToBytes(_bool : Bool) : [Nat8]{ + + if(_bool == true){ + return [1:Nat8]; + } else { + return [0:Nat8]; + }; + }; + + public func bytesToBool(_bytes : [Nat8]) : Bool{ + + if(_bytes[0] == 0){ + return false; + } else { + return true; + }; + }; + + public func intToBytes(n : Int) : [Nat8]{ + + var a : Nat8 = 0; + var c : Nat8 = if(n < 0){1}else{0}; + var b : Nat = Int.abs(n); + var bytes = List.nil(); + var test = true; + while test { + a := Nat8.fromNat(b % 128); + b := b / 128; + bytes := List.push(a, bytes); + test := b > 0; + }; + let result = Utils.toBuffer([c]); + result.append(Utils.toBuffer(List.toArray(bytes))); + result.toArray(); + }; + + public func bytesToInt(_bytes : [Nat8]) : Int{ + + var n : Int = 0; + var i = 0; + let natBytes = Array.tabulate(_bytes.size() - 2, func(idx){_bytes[idx+1]}); + + Array.foldRight(natBytes, (), func (byte, _) { + n += Nat8.toNat(byte) * 128 ** i; + i += 1; + return; + }); + if(_bytes[0]==1){ + n *= -1; + }; + return n; + }; + + public func bytesToNat64Array(array: [Nat8]) : [Nat64] { + assert(array.size() % 8 == 0); + let size = array.size() / 8; + let buffer = Buffer.Buffer(size); + for (idx in Iter.range(0, size - 1)){ + buffer.add( + (Nat64.fromNat(Nat8.toNat(array[idx])) << 56) + + (Nat64.fromNat(Nat8.toNat(array[idx + 1])) << 48) + + (Nat64.fromNat(Nat8.toNat(array[idx + 2])) << 40) + + (Nat64.fromNat(Nat8.toNat(array[idx + 3])) << 32) + + (Nat64.fromNat(Nat8.toNat(array[idx + 4])) << 24) + + (Nat64.fromNat(Nat8.toNat(array[idx + 5])) << 16) + + (Nat64.fromNat(Nat8.toNat(array[idx + 6])) << 8) + + (Nat64.fromNat(Nat8.toNat(array[idx + 7])))); + }; + buffer.toArray(); + }; + + public func nat64ArrayToBytes(array: [Nat64]) : [Nat8] { + let buffer = Buffer.Buffer<[Nat8]>(array.size() * 8); + for (nat64 in Array.vals(array)){ + buffer.add(nat64ToBytes(nat64)); + }; + Array.flatten(buffer.toArray()); + }; + +}; \ No newline at end of file diff --git a/src/StableBTree/iter.mo b/src/StableBTree/iter.mo new file mode 100644 index 00000000..c797a0bf --- /dev/null +++ b/src/StableBTree/iter.mo @@ -0,0 +1,186 @@ +import Types "types"; +import Node "node"; +import Constants "constants"; +import Utils "utils"; + +import Nat64 "mo:base/Nat64"; +import Debug "mo:base/Debug"; +import Stack "mo:base/Stack"; +import Array "mo:base/Array"; +import Nat8 "mo:base/Nat8"; +import Order "mo:base/Order"; + +module { + + // For convenience: from types module + type IBTreeMap = Types.IBTreeMap; + type Cursor = Types.Cursor; + // For convenience: from node module + type Node = Node.Node; + + public func new(map: IBTreeMap) : Iter{ + Iter({ + map; + // Initialize the cursors with the address of the root of the map. + cursors = [#Address(map.getRootAddr())]; + prefix = null; + offset = null; + }); + }; + + public func empty(map: IBTreeMap) : Iter{ + Iter({ + map; + cursors = []; + prefix = null; + offset = null; + }); + }; + + public func newWithPrefix(map: IBTreeMap, prefix: [Nat8], cursors: [Cursor]) : Iter{ + Iter({ + map; + cursors; + prefix = ?prefix; + offset = null; + }); + }; + + public func newWithPrefixAndOffset(map: IBTreeMap, prefix: [Nat8], offset: [Nat8], cursors: [Cursor]) : Iter{ + Iter({ + map; + cursors; + prefix = ?prefix; + offset = ?offset; + }); + }; + + type IterVariables = { + map: IBTreeMap; + cursors: [Cursor]; + prefix: ?[Nat8]; + offset: ?[Nat8]; + }; + + /// An iterator over the entries of a [`BTreeMap`]. + /// Iterators are lazy and do nothing unless consumed + public class Iter(variables: IterVariables) = self { + + // A reference to the map being iterated on. + let map_: IBTreeMap = variables.map; + + // A stack of cursors indicating the current position in the tree. + var cursors_ = Stack.Stack(); + for (cursor in Array.vals(variables.cursors)) { + cursors_.push(cursor); + }; + + // An optional prefix that the keys of all the entries returned must have. + // Iteration stops as soon as it runs into a key that doesn't have this prefix. + let prefix_: ?[Nat8] = variables.prefix; + + // An optional offset to begin iterating from in the keys with the same prefix. + // Used only in the case that prefix is also set. + let offset_: ?[Nat8] = variables.offset; + + public func next() : ?(K, V) { + switch(cursors_.pop()) { + case(?cursor){ + switch(cursor){ + case(#Address(address)){ + if (address != Constants.NULL){ + // Load the node at the given address, and add it to the cursors. + let node = map_.loadNode(address); + + cursors_.push(#Node{ + next = switch(node.getNodeType()) { + // Iterate on internal nodes starting from the first child. + case(#Internal) { #Child(0); }; + // Iterate on leaf nodes starting from the first entry. + case(#Leaf) { #Entry(0); }; + }; + node; + }); + }; + return self.next(); + }; + case(#Node({node; next;})){ + switch(next){ + case(#Child(child_idx)){ + if (Nat64.toNat(child_idx) >= node.getChildren().size()){ + Debug.trap("Iterating over children went out of bounds."); + }; + + // After iterating on the child, iterate on the next _entry_ in this node. + // The entry immediately after the child has the same index as the child's. + cursors_.push(#Node { + node; + next = #Entry(child_idx); + }); + + // Add the child to the top of the cursors to be iterated on first. + let child_address = node.getChild(Nat64.toNat(child_idx)); + cursors_.push(#Address(child_address)); + + return self.next(); + }; + case(#Entry(entry_idx)){ + if (Nat64.toNat(entry_idx) >= node.getEntries().size()) { + // No more entries to iterate on in this node. + return self.next(); + }; + + // Take the entry from the node. It's swapped with an empty element to + // avoid cloning. + let entry = node.swapEntry(Nat64.toNat(entry_idx), ([], [])); + + // Add to the cursors the next element to be traversed. + cursors_.push(#Node { + next = switch(node.getNodeType()){ + // If this is an internal node, add the next child to the cursors. + case(#Internal) { #Child(entry_idx + 1); }; + // If this is a leaf node, add the next entry to the cursors. + case(#Leaf) { #Entry(entry_idx + 1); }; + }; + node; + }); + + // If there's a prefix, verify that the key has that given prefix. + // Otherwise iteration is stopped. + switch(prefix_){ + case(null) {}; + case(?prefix){ + if (not Utils.isPrefixOf(prefix, entry.0, Nat8.equal)){ + // Clear all cursors to avoid needless work in subsequent calls. + cursors_ := Stack.Stack(); + return null; + } else switch(offset_) { + case(null) {}; + case(?offset){ + let prefix_with_offset = Utils.toBuffer(prefix); + prefix_with_offset.append(Utils.toBuffer(offset)); + // Clear all cursors to avoid needless work in subsequent calls. + if (Order.isLess(Node.compareEntryKeys(entry.0, prefix_with_offset.toArray()))){ + cursors_ := Stack.Stack(); + return null; + }; + }; + }; + }; + }; + return ?(map_.getKeyConverter().fromBytes(entry.0), map_.getValueConverter().fromBytes(entry.1)); + }; + }; + }; + }; + }; + case(null){ + // The cursors are empty. Iteration is complete. + null; + }; + }; + }; + + }; + +}; \ No newline at end of file diff --git a/src/StableBTree/memory.mo b/src/StableBTree/memory.mo new file mode 100644 index 00000000..f314b035 --- /dev/null +++ b/src/StableBTree/memory.mo @@ -0,0 +1,150 @@ +import Constants "constants"; +import Utils "utils"; +import Types "types"; + +import StableMemory "mo:base/ExperimentalStableMemory"; +import Blob "mo:base/Blob"; +import Int64 "mo:base/Int64"; +import Buffer "mo:base/Buffer"; +import Nat64 "mo:base/Nat64"; +import Array "mo:base/Array"; +import Debug "mo:base/Debug"; +import Iter "mo:base/Iter"; +import Text "mo:base/Text"; +import Nat8 "mo:base/Nat8"; +import Result "mo:base/Result"; + +module { + + // For convenience: from types module + type Memory = Types.Memory; + // For convenience: from base module + type Result = Result.Result; + + type GrowFailed = { + current_size: Nat64; + delta: Nat64; + }; + + /// Writes the bytes at the specified address, growing the memory size if needed. + public func safeWrite(memory: Memory, address: Nat64, bytes: [Nat8]) : Result<(), GrowFailed> { + // Traps on overflow. + let offset = address + Nat64.fromNat(bytes.size()); + // Compute the number of pages required. + let pages = (offset + Constants.WASM_PAGE_SIZE - 1) / Constants.WASM_PAGE_SIZE; + // Grow the number of pages if necessary. + if (pages > memory.size()){ + let diff_pages = pages - memory.size(); + if (memory.grow(diff_pages) < 0){ + return #err({ + current_size = memory.size(); + delta = diff_pages; + }); + }; + }; + // Store the bytes in memory. + memory.write(address, bytes); + #ok(); + }; + + /// Like [safe_write], but traps if the memory.grow fails. + public func write(memory: Memory, address: Nat64, bytes: [Nat8]) { + switch(safeWrite(memory, address, bytes)){ + case(#err({current_size; delta})){ + Debug.trap("Failed to grow memory from " # Nat64.toText(current_size) + # " pages to " # Nat64.toText(current_size + delta) + # " pages (delta = " # Nat64.toText(delta) # " pages)."); + }; + case(_) {}; + }; + }; + + /// Reads the bytes at the specified address, traps if exceeds memory size. + public func read(memory: Memory, address: Nat64, size: Nat) : [Nat8] { + memory.read(address, size); + }; + + public let STABLE_MEMORY = object { + public func size() : Nat64 { + StableMemory.size(); + }; + public func grow(pages: Nat64) : Int64 { + let old_size = StableMemory.grow(pages); + if (old_size == 0xFFFF_FFFF_FFFF_FFFF){ + return -1; + }; + Int64.fromNat64(old_size); + }; + public func write(address: Nat64, bytes: [Nat8]) { + StableMemory.storeBlob(address, Blob.fromArray(bytes)); + }; + public func read(address: Nat64, size: Nat) : [Nat8] { + Blob.toArray(StableMemory.loadBlob(address, size)); + }; + }; + + public class VecMemory() = this { + + // 2^64 - 1 = 18446744073709551615 + let MAX_PAGES : Nat64 = 18446744073709551615 / Constants.WASM_PAGE_SIZE; + + let buffer_ = Buffer.Buffer(0); + + public func size() : Nat64 { + Nat64.fromNat(buffer_.size()) / Constants.WASM_PAGE_SIZE; + }; + + public func grow(pages: Nat64) : Int64 { + let size = this.size(); + let num_pages = size + pages; + // Number of pages cannot exceed defined MAX_PAGES. + if (num_pages > MAX_PAGES) { + return -1; + }; + // Add the pages (initialized with zeros) to the memory buffer. + let to_add = Array.freeze(Array.init(Nat64.toNat(pages * Constants.WASM_PAGE_SIZE), 0)); + buffer_.append(Utils.toBuffer(to_add)); + // Return the previous size. + return Int64.fromIntWrap(Nat64.toNat(size)); + }; + + public func read(address: Nat64, size: Nat) : [Nat8] { + // Traps on overflow. + let offset = Nat64.toNat(address) + size; + // Cannot read pass the memory buffer size. + if (offset > buffer_.size()){ + Debug.trap("read: out of bounds"); + }; + // Copy the bytes from the memory buffer. + let bytes = Buffer.Buffer(size); + for (idx in Iter.range(Nat64.toNat(address), offset - 1)){ + bytes.add(buffer_.get(idx)); + }; + bytes.toArray(); + }; + + public func write(address: Nat64, bytes: [Nat8]) { + let offset = Nat64.toNat(address) + bytes.size(); + // Check that the bytes fit into the buffer. + if (offset > buffer_.size()){ + Debug.trap("write: out of bounds"); + }; + // Copy the given bytes in the memory buffer. + for (idx in Array.keys(bytes)){ + buffer_.put(Nat64.toNat(address) + idx, bytes[idx]); + }; + }; + + public func toText() : Text { + let text_buffer = Buffer.Buffer(0); + text_buffer.add("Memory : ["); + for (byte in buffer_.vals()){ + text_buffer.add(Nat8.toText(byte) # ", "); + }; + text_buffer.add("]"); + Text.join("", text_buffer.vals()); + }; + + }; + +}; \ No newline at end of file diff --git a/src/StableBTree/memoryManager.mo b/src/StableBTree/memoryManager.mo new file mode 100644 index 00000000..839d2a00 --- /dev/null +++ b/src/StableBTree/memoryManager.mo @@ -0,0 +1,564 @@ +//! A module for simulating multiple memories within a single memory. +//! +//! The typical way for a canister to have multiple stable structures is by dividing the memory into +//! distinct ranges, dedicating each range to a stable structure. This approach has two problems: +//! +//! 1. The developer needs to put in advance an upper bound on the memory of each stable structure. +//! 2. It wastes the canister's memory allocation. For example, if a canister create twos two stable +//! structures A and B, and gives each one of them a 1GiB region of memory, then writing to B will +//! require growing > 1GiB of memory just to be able to write to it. +//! +//! The [`MemoryManager`] in this module solves both of these problems. It simulates having +//! multiple memories, each being able to grow without bound. That way, a developer doesn't need to +//! put an upper bound to how much stable structures can grow, and the canister's memory allocation +//! becomes less wasteful. +//! +//! Example Usage: +//! +//! ``` +//! use ic_stable_structures::{DefaultMemoryImpl, Memory}; +//! use ic_stable_structures::memory_manager::{MemoryManager, MemoryId}; +//! +//! let mem_mgr = MemoryManager::init(DefaultMemoryImpl::default()); +//! +//! // Create different memories, each with a unique ID. +//! let memory_0 = mem_mgr.get(MemoryId::new(0)); +//! let memory_1 = mem_mgr.get(MemoryId::new(1)); +//! +//! // Each memory can be used independently. +//! memory_0.grow(1); +//! memory_0.write(0, &[1, 2, 3]); +//! +//! memory_1.grow(1); +//! memory_1.write(0, &[4, 5, 6]); +//! +//! var bytes = vec![0; 3]; +//! memory_0.read(0, &mut bytes); +//! assert_eq!(bytes, vec![1, 2, 3]); +//! +//! var bytes = vec![0; 3]; +//! memory_1.read(0, &mut bytes); +//! assert_eq!(bytes, vec![4, 5, 6]); +//! ``` + +import Constants "constants"; +import Conversion "conversion"; +import Types "types"; +import Memory "memory"; + +import RBTree "mo:base/RBTree"; +import Array "mo:base/Array"; +import Nat8 "mo:base/Nat8"; +import Nat16 "mo:base/Nat16"; +import Nat64 "mo:base/Nat64"; +import Int64 "mo:base/Int64"; +import Iter "mo:base/Iter"; +import Blob "mo:base/Blob"; +import Debug "mo:base/Debug"; +import Option "mo:base/Option"; +import Buffer "mo:base/Buffer"; +import Text "mo:base/Text"; + +module { + + // For convenience: from types module + type Address = Types.Address; + type Memory = Types.Memory; + type Bytes = Types.Bytes; + // For convenience: from base module + type RBTree = RBTree.RBTree; + type Buffer = Buffer.Buffer; + + let MAGIC = "MGR"; + let LAYOUT_VERSION: Nat8 = 1; + + // The maximum number of memories that can be created. + public let MAX_NUM_MEMORIES: Nat8 = 255; + + // The maximum number of buckets the memory manager can handle. + // With a bucket size of 1024 pages this can support up to 2TiB of memory. + public let MAX_NUM_BUCKETS: Nat64 = 32768; + + public let BUCKET_SIZE_IN_PAGES: Nat64 = 1024; + + // A value used internally to indicate that a bucket is unallocated. + public let UNALLOCATED_BUCKET_MARKER: Nat8 = MAX_NUM_MEMORIES; + + // The offset where buckets are in memory. + let BUCKETS_OFFSET_IN_PAGES: Nat64 = 1; + + // Reserved bytes in the header for future extensions. + let HEADER_RESERVED_BYTES: Nat = 32; + + /// A memory manager simulates multiple memories within a single memory. + /// + /// The memory manager can return up to 255 unique instances of [`VirtualMemory`], and each can be + /// used independently and can grow up to the bounds of the underlying memory. + /// + /// The memory manager divides the memory into "buckets" of 1024 pages. Each [`VirtualMemory`] is + /// internally represented as a list of buckets. Buckets of different memories can be interleaved, + /// but the [`VirtualMemory`] interface gives the illusion of a continuous address space. + /// + /// Because a [`VirtualMemory`] is a list of buckets, this implies that internally it grows one + /// bucket at time (1024 pages). This implication makes the memory manager ideal for a small number + /// of memories storing large amounts of data, as opposed to a large number of memories storing + /// small amounts of data. + /// + /// The first page of the memory is reserved for the memory manager's own state. The layout for + /// this state is as follows: + /// + /// # V1 layout + /// + /// ```text + /// -------------------------------------------------- <- Address 0 + /// Magic "MGR" ↕ 3 bytes + /// -------------------------------------------------- + /// Layout version ↕ 1 byte + /// -------------------------------------------------- + /// Number of allocated buckets ↕ 2 bytes + /// -------------------------------------------------- + /// Max number of buckets = N ↕ 2 bytes + /// -------------------------------------------------- + /// Reserved space ↕ 32 bytes + /// -------------------------------------------------- + /// Size of memory 0 (in pages) ↕ 8 bytes + /// -------------------------------------------------- + /// Size of memory 1 (in pages) ↕ 8 bytes + /// -------------------------------------------------- + /// ... + /// -------------------------------------------------- + /// Size of memory 254 (in pages) ↕ 8 bytes + /// -------------------------------------------------- <- Bucket allocations + /// Bucket 1 ↕ 1 byte (1 byte indicating which memory owns it) + /// -------------------------------------------------- + /// Bucket 2 ↕ 1 byte + /// -------------------------------------------------- + /// ... + /// -------------------------------------------------- + /// Bucket `MAX_NUM_BUCKETS` ↕ 1 byte + /// -------------------------------------------------- + /// Unallocated space + /// -------------------------------------------------- <- Buckets (Page 1) + /// Bucket 1 ↕ 1024 pages + /// -------------------------------------------------- <- Page 1025 + /// Bucket 2 ↕ 1024 pages + /// -------------------------------------------------- + /// ... + /// -------------------------------------------------- <- Page ((N - 1) * 1024 + 1) + /// Bucket N ↕ 1024 pages + /// ``` + + /// Initializes a `MemoryManager` with the given memory. + public func init(memory: Memory) : MemoryManager { + initWithBuckets(memory, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + }; + + public func initWithBuckets(memory: Memory, bucket_size_in_pages: Nat16) : MemoryManager { + MemoryManager(initInner(memory, bucket_size_in_pages)); + }; + + public class MemoryManager(inner : MemoryManagerInner) { + + public let inner_ : MemoryManagerInner = inner; + + /// Returns the memory associated with the given ID. + public func get(id: MemoryId) : VirtualMemory { + VirtualMemory(id, inner_); + }; + + }; + + type Header = { + magic: [Nat8]; // 3 bytes + version: Nat8; + // The number of buckets allocated by the memory manager. + num_allocated_buckets: Nat16; + // The size of a bucket in Wasm pages. + bucket_size_in_pages: Nat16; + // Reserved bytes for future extensions + _reserved: [Nat8]; // HEADER_RESERVED_BYTES bytes = 32 bytes + // The size of each individual memory that can be created by the memory manager. + memory_sizes_in_pages: [Nat64]; // (MAX_NUM_MEMORIES * 8) bytes = (255 * 8) bytes = 2040 bytes + }; + + let SIZE_HEADER : Nat64 = 2080; + + public func saveManagerHeader(header: Header, addr: Address, memory: Memory) { + Memory.write(memory, addr , header.magic); + Memory.write(memory, addr + 3 , [header.version]); + Memory.write(memory, addr + 3 + 1 , Conversion.nat16ToBytes(header.num_allocated_buckets)); + Memory.write(memory, addr + 3 + 1 + 2 , Conversion.nat16ToBytes(header.bucket_size_in_pages)); + Memory.write(memory, addr + 3 + 1 + 2 + 2 , header._reserved); + Memory.write(memory, addr + 3 + 1 + 2 + 2 + 32, Conversion.nat64ArrayToBytes(header.memory_sizes_in_pages)); + }; + + public func loadManagerHeader(addr: Address, memory: Memory) : Header { + { + magic = Memory.read(memory, addr, 3); + version = Memory.read(memory, addr + 3, 1)[0]; + num_allocated_buckets = Conversion.bytesToNat16(Memory.read(memory, addr + 3 + 1, 2)); + bucket_size_in_pages = Conversion.bytesToNat16(Memory.read(memory, addr + 3 + 1 + 2, 2)); + _reserved = Memory.read(memory, addr + 3 + 1 + 2 + 2, 32); + memory_sizes_in_pages = Conversion.bytesToNat64Array(Memory.read(memory, addr + 3 + 1 + 2 + 2 + 32, 2040)); + }; + }; + + public class VirtualMemory(id: MemoryId, memory_manager: MemoryManagerInner) { + + // Assert the id is correct + verifyId(id); + + public let id_ : MemoryId = id; + public let memory_manager_ : MemoryManagerInner = memory_manager; + + public func size() : Nat64 { + memory_manager_.memorySize(id_); + }; + + public func grow(pages: Nat64) : Int64 { + memory_manager_.grow(id_, pages); + }; + + public func read(offset: Nat64, size: Nat) : [Nat8] { + memory_manager_.read(id_, offset, size); + }; + + public func write(offset: Nat64, src: [Nat8]) { + memory_manager_.write(id_, offset, src); + }; + + }; + + public func initInner(memory: Memory, bucket_size_in_pages: Nat16) : MemoryManagerInner { + if (memory.size() == 0) { + // Memory is empty. Create a new map. + return newInner(memory, bucket_size_in_pages); + }; + + // Check if the magic in the memory corresponds to this object. + let dst = Memory.read(memory, 0, 3); + if (dst != Blob.toArray(Text.encodeUtf8(MAGIC))) { + // No memory manager found. Create a new instance. + return newInner(memory, bucket_size_in_pages); + } else { + // The memory already contains a memory manager. Load it. + let mem_mgr = loadInner(memory); + + // Assert that the bucket size passed is the same as the one previously stored. + if (mem_mgr.getBucketSizeInPages() != bucket_size_in_pages){ + Debug.trap("The bucket size of the loaded memory manager is different than the given one."); + }; + + mem_mgr; + }; + }; + + public func newInner(memory: Memory, bucket_size_in_pages: Nat16) : MemoryManagerInner { + let mem_mgr = MemoryManagerInner( + memory, + 0, + bucket_size_in_pages, + Array.init(Nat8.toNat(MAX_NUM_MEMORIES), 0), + RBTree.RBTree>(Nat8.compare) + ); + + mem_mgr.saveHeader(); + + // Mark all the buckets as unallocated. + Memory.write( + memory, + bucketAllocationsAddress(0), + Array.tabulate(Nat8.toNat(MAX_NUM_MEMORIES), func(index: Nat) : Nat8 { UNALLOCATED_BUCKET_MARKER; }) + ); + + mem_mgr; + }; + + public func loadInner(memory: Memory) : MemoryManagerInner { + // Read the header from memory. + let header = loadManagerHeader(Constants.ADDRESS_0, memory); + if (header.magic != Blob.toArray(Text.encodeUtf8(MAGIC))) { Debug.trap("Bad magic."); }; + if (header.version != LAYOUT_VERSION) { Debug.trap("Unsupported version."); }; + + let buckets = Memory.read(memory, bucketAllocationsAddress(0), Nat64.toNat(MAX_NUM_BUCKETS)); + + let memory_buckets = RBTree.RBTree>(Nat8.compare); + + for (bucket_idx in Array.keys(buckets)){ + let memory = buckets[bucket_idx]; + if (memory != UNALLOCATED_BUCKET_MARKER){ + let buckets = Option.get(memory_buckets.get(memory), Buffer.Buffer(1)); + buckets.add(Nat16.fromNat(bucket_idx)); + memory_buckets.put(memory, buckets); + }; + }; + + MemoryManagerInner( + memory, + header.num_allocated_buckets, + header.bucket_size_in_pages, + Array.thaw(header.memory_sizes_in_pages), + memory_buckets + ); + }; + + public class MemoryManagerInner( + memory: Memory, + allocated_buckets: Nat16, + bucket_size_in_pages: Nat16, + memory_sizes_in_pages: [var Nat64], + memory_buckets: RBTree> + ){ + // Make sure the array of memory sizes has the correct size. + assert(memory_sizes_in_pages.size() == Nat8.toNat(MAX_NUM_MEMORIES)); + + public let memory_ : Memory = memory; + // The number of buckets that have been allocated. + var allocated_buckets_ : Nat16 = allocated_buckets; + let bucket_size_in_pages_ : Nat16 = bucket_size_in_pages; + // An array storing the size (in pages) of each of the managed memories. + let memory_sizes_in_pages_ : [var Nat64] = memory_sizes_in_pages; + // A map mapping each managed memory to the bucket ids that are allocated to it. + public let memory_buckets_ : RBTree> = memory_buckets; + + public func getBucketSizeInPages() : Nat16 { + bucket_size_in_pages_; + }; + + public func saveHeader() { + let header = { + magic = Blob.toArray(Text.encodeUtf8(MAGIC)); + version = LAYOUT_VERSION; + num_allocated_buckets = allocated_buckets_; + bucket_size_in_pages = bucket_size_in_pages_; + _reserved = Array.tabulate(HEADER_RESERVED_BYTES, func(index: Nat) : Nat8 { 0; }); + memory_sizes_in_pages = Array.freeze(memory_sizes_in_pages_); + }; + + saveManagerHeader(header, Constants.ADDRESS_0, memory_); + }; + + // Returns the size of a memory (in pages). + public func memorySize(id: MemoryId) : Nat64 { + verifyId(id); + + memory_sizes_in_pages_[Nat8.toNat(id)]; + }; + + // Grows the memory with the given id by the given number of pages. + public func grow(id: MemoryId, pages: Nat64) : Int64 { + verifyId(id); + + // Compute how many additional buckets are needed. + let old_size = memorySize(id); + let new_size = old_size + pages; + let current_buckets = numBucketsNeeded(old_size); + let required_buckets = numBucketsNeeded(new_size); + let new_buckets_needed = required_buckets - current_buckets; + + if (new_buckets_needed + Nat64.fromNat(Nat16.toNat(allocated_buckets_)) > MAX_NUM_BUCKETS) { + // Exceeded the memory that can be managed. + return -1; + }; + + // Allocate new buckets as needed. + for (_ in Iter.range(0, Nat64.toNat(new_buckets_needed) - 1)) { + let new_bucket_id = allocated_buckets_; + + let buckets = Option.get(memory_buckets_.get(id), Buffer.Buffer(1)); + buckets.add(new_bucket_id); + memory_buckets_.put(id, buckets); + + // Write in stable store that this bucket belongs to the memory with the provided `id`. + Memory.write(memory_, bucketAllocationsAddress(new_bucket_id), [id]); + + allocated_buckets_ += 1; + }; + + // Grow the underlying memory if necessary. + let pages_needed = BUCKETS_OFFSET_IN_PAGES + + Nat64.fromNat(Nat16.toNat(bucket_size_in_pages_)) * Nat64.fromNat(Nat16.toNat(allocated_buckets_)); + if (pages_needed > memory_.size()) { + let additional_pages_needed = pages_needed - memory_.size(); + if (memory_.grow(additional_pages_needed) == -1){ + Debug.trap(Nat8.toText(id) # ": grow failed"); + }; + }; + + // Update the memory with the new size. + memory_sizes_in_pages_[Nat8.toNat(id)] := new_size; + + // Update the header and return the old size. + saveHeader(); + Int64.fromNat64(old_size); + }; + + public func write(id: MemoryId, offset: Nat64, src: [Nat8]) { + verifyId(id); + + if ((offset + Nat64.fromNat(src.size())) > memorySize(id) * Constants.WASM_PAGE_SIZE) { + Debug.trap(Nat8.toText(id) # ": write out of bounds"); + }; + + var bytes_written : Nat = 0; + for ({address; length;} in bucketIter(id, offset, src.size())) { + Memory.write( + memory_, + address, + Array.tabulate(Nat64.toNat(length), func(idx: Nat) : Nat8 { src[bytes_written + idx]; }) + ); + + bytes_written += Nat64.toNat(length); + }; + }; + + public func read(id: MemoryId, offset: Nat64, size: Nat) : [Nat8] { + verifyId(id); + if ((offset + Nat64.fromNat(size)) > memorySize(id) * Constants.WASM_PAGE_SIZE) { + Debug.trap(Nat8.toText(id) # ": read out of bounds"); + }; + let buffer = Buffer.Buffer<[Nat8]>(0); + for ({address; length;} in bucketIter(id, offset, size)) { + buffer.add(Memory.read( + memory_, + address, + Nat64.toNat(length), + )); + }; + Array.flatten(buffer.toArray()); + }; + + // Initializes a [`BucketIterator`]. + public func bucketIter(id: MemoryId, offset: Nat64, length: Nat) : BucketIterator { + verifyId(id); + + // Get the buckets allocated to the given memory id. + let buckets = Option.get(memory_buckets_.get(id), Buffer.Buffer(0)); + + BucketIterator( + { + address = offset; + length = Nat64.fromNat(length); + }, + buckets.toArray(), + bucketSizeInBytes() + ); + }; + + public func bucketSizeInBytes() : Bytes { + Nat64.fromNat(Nat16.toNat(bucket_size_in_pages_)) * Constants.WASM_PAGE_SIZE; + }; + + // Returns the number of buckets needed to accommodate the given number of pages. + public func numBucketsNeeded(num_pages: Nat64) : Nat64 { + // Ceiling division. + let bucket_size_in_pages = Nat64.fromNat(Nat16.toNat(bucket_size_in_pages_)); + (num_pages + bucket_size_in_pages - 1) / bucket_size_in_pages; + }; + + }; + + public type Segment = { + address: Address; + length: Bytes; + }; + + // An iterator that maps a segment of virtual memory to segments of real memory. + // + // A segment in virtual memory can map to multiple segments of real memory. Here's an example: + // + // Virtual Memory + // -------------------------------------------------------- + // (A) ---------- SEGMENT ---------- (B) + // -------------------------------------------------------- + // ↑ ↑ ↑ ↑ + // Bucket 0 Bucket 1 Bucket 2 Bucket 3 + // + // The [`VirtualMemory`] is internally divided into fixed-size buckets. In the memory's virtual + // address space, all these buckets are consecutive, but in real memory this may not be the case. + // + // A virtual segment would first be split at the bucket boundaries. The example virtual segment + // above would be split into the following segments: + // + // (A, end of bucket 0) + // (start of bucket 1, end of bucket 1) + // (start of bucket 2, B) + // + // Each of the segments above can then be translated into the real address space by looking up + // the underlying buckets' addresses in real memory. + public class BucketIterator ( + virtual_segment : Segment, + buckets : [BucketId], + bucket_size_in_bytes : Bytes + ) { + + type Item = Segment; + + var virtual_segment_: Segment = virtual_segment; + public let buckets_: [BucketId] = buckets; + public let bucket_size_in_bytes_: Bytes = bucket_size_in_bytes; + + public func next() : ?Item { + if (virtual_segment_.length == 0) { + return null; + }; + + // Map the virtual segment's address to a real address. + let bucket_idx = Nat64.toNat(virtual_segment_.address / bucket_size_in_bytes_); + if (bucket_idx >= buckets_.size()){ + Debug.trap("bucket idx out of bounds"); + }; + + let bucket_address = bucketAddress(buckets_[bucket_idx]); + + let real_address = bucket_address + + virtual_segment_.address % bucket_size_in_bytes_; + + // Compute how many bytes are in this real segment. + let bytes_in_segment = do { + let next_bucket_address = bucket_address + bucket_size_in_bytes_; + + // Write up to either the end of the bucket, or the end of the segment. + Nat64.min( + next_bucket_address - real_address, + virtual_segment_.length + ); + }; + + // Update the virtual segment to exclude the portion we're about to return. + virtual_segment_ := { + length = virtual_segment_.length - bytes_in_segment; + address = virtual_segment_.address + bytes_in_segment; + }; + + ?{ + address = real_address; + length = bytes_in_segment; + }; + }; + + // Returns the address of a given bucket. + public func bucketAddress(id: BucketId) : Address { + let bucket_offset_in_bytes = BUCKETS_OFFSET_IN_PAGES * Constants.WASM_PAGE_SIZE; + bucket_offset_in_bytes + bucket_size_in_bytes_ * Nat64.fromNat(Nat16.toNat(id)); + }; + + }; + + public type MemoryId = Nat8; + + public func verifyId(id: MemoryId) { + // Any ID can be used except the special value that's used internally to + // mark a bucket as unallocated. + if(id == UNALLOCATED_BUCKET_MARKER){ + Debug.trap("Memory ID cannot be equal to " # Nat8.toText(UNALLOCATED_BUCKET_MARKER)); + }; + }; + + public type BucketId = Nat16; + + public func bucketAllocationsAddress(id: BucketId) : Address { + Constants.ADDRESS_0 + SIZE_HEADER + Nat64.fromNat(Nat16.toNat(id)); + }; + +}; diff --git a/src/StableBTree/node.mo b/src/StableBTree/node.mo new file mode 100644 index 00000000..e6536978 --- /dev/null +++ b/src/StableBTree/node.mo @@ -0,0 +1,424 @@ +import Types "types"; +import Conversion "conversion"; +import Constants "constants"; +import Utils "utils"; +import Memory "memory"; + +import Blob "mo:base/Blob"; +import Text "mo:base/Text"; +import Debug "mo:base/Debug"; +import Buffer "mo:base/Buffer"; +import Array "mo:base/Array"; +import Iter "mo:base/Iter"; +import Nat8 "mo:base/Nat8"; +import Nat16 "mo:base/Nat16"; +import Nat32 "mo:base/Nat32"; +import Nat64 "mo:base/Nat64"; +import Result "mo:base/Result"; +import Order "mo:base/Order"; + +module { + + // For convenience: from base module + type Result = Result.Result; + type Buffer = Buffer.Buffer; + type Order = Order.Order; + // For convenience: from types module + type Address = Types.Address; + type Bytes = Types.Bytes; + type Memory = Types.Memory; + type Entry = Types.Entry; + type NodeType = Types.NodeType; + + let LAYOUT_VERSION: Nat8 = 1; + let MAGIC = "BTN"; + let LEAF_NODE_TYPE: Nat8 = 0; + let INTERNAL_NODE_TYPE: Nat8 = 1; + // The size of Nat32 in bytes. + let U32_SIZE: Nat = 4; + // The size of an address in bytes. + let ADDRESS_SIZE: Nat = 8; + + /// Loads a node from memory at the given address. + public func load( + address: Address, + memory: Memory, + max_key_size: Nat32, + max_value_size: Nat32 + ) : Node { + + // Load the header. + let header = loadNodeHeader(address, memory); + if (header.magic != Blob.toArray(Text.encodeUtf8(MAGIC))) { Debug.trap("Bad magic."); }; + if (header.version != LAYOUT_VERSION) { Debug.trap("Unsupported version."); }; + + // Load the entries. + var entries = Buffer.Buffer(0); + var offset = SIZE_NODE_HEADER; + for (_ in Iter.range(0, Nat16.toNat(header.num_entries - 1))){ + // Read the key's size. + let key_size = Conversion.bytesToNat32(Memory.read(memory, address + offset, U32_SIZE)); + offset += Nat64.fromNat(U32_SIZE); + + // Read the key. + let key = Memory.read(memory, address + offset, Nat32.toNat(key_size)); + offset += Nat64.fromNat(Nat32.toNat(max_key_size)); + + // Read the value's size. + let value_size = Conversion.bytesToNat32(Memory.read(memory, address + offset, U32_SIZE)); + offset += Nat64.fromNat(U32_SIZE); + + // Read the value. + let value = Memory.read(memory, address + offset, Nat32.toNat(value_size)); + offset += Nat64.fromNat(Nat32.toNat(max_value_size)); + + entries.add((key, value)); + }; + + // Load children if this is an internal + var children = Buffer.Buffer
(0); + if (header.node_type == INTERNAL_NODE_TYPE) { + // The number of children is equal to the number of entries + 1. + for (_ in Iter.range(0, Nat16.toNat(header.num_entries))){ + let child = Conversion.bytesToNat64(Memory.read(memory, address + offset, ADDRESS_SIZE)); + offset += Nat64.fromNat(ADDRESS_SIZE); + children.add(child); + }; + assert(children.size() == entries.size() + 1); + }; + + Node({ + address; + entries = entries.toArray(); + children = children.toArray(); + node_type = getNodeType(header); + max_key_size; + max_value_size; + }); + }; + + /// Returns the size of a node in bytes. + /// + /// See the documentation of [`Node`] for the memory layout. + public func size(max_key_size: Nat32, max_value_size: Nat32) : Bytes { + let max_key_size_n64 = Nat64.fromNat(Nat32.toNat(max_key_size)); + let max_value_size_n64 = Nat64.fromNat(Nat32.toNat(max_value_size)); + + let node_header_size = SIZE_NODE_HEADER; + let entry_size = Nat64.fromNat(U32_SIZE) + max_key_size_n64 + max_value_size_n64 + Nat64.fromNat(U32_SIZE); + let child_size = Nat64.fromNat(ADDRESS_SIZE); + + node_header_size + + getCapacity() * entry_size + + (getCapacity() + 1) * child_size; + }; + + type NodeVariables = { + address: Address; + entries: [Entry]; + children: [Address]; + node_type: NodeType; + max_key_size: Nat32; + max_value_size: Nat32; + }; + + /// A node of a B-Tree. + /// + /// The node is stored in stable memory with the following layout: + /// + /// | NodeHeader | Entries (keys and values) | Children | + /// + /// Each node contains up to `CAPACITY` entries, each entry contains: + /// - size of key (4 bytes) + /// - key (`max_key_size` bytes) + /// - size of value (4 bytes) + /// - value (`max_value_size` bytes) + /// + /// Each node can contain up to `CAPACITY + 1` children, each child is 8 bytes. + public class Node(variables : NodeVariables) { + + /// Members + var address_ : Address = variables.address; + var entries_ : Buffer = Utils.toBuffer(variables.entries); + var children_ : Buffer
= Utils.toBuffer(variables.children); + let node_type_ : NodeType = variables.node_type; + let max_key_size_ : Nat32 = variables.max_key_size; + let max_value_size_ : Nat32 = variables.max_value_size; + + /// Getters + public func getAddress() : Address { address_; }; + public func getEntries() : Buffer { entries_; }; + public func getChildren() : Buffer
{ children_; }; + public func getNodeType() : NodeType { node_type_; }; + public func getMaxKeySize() : Nat32 { max_key_size_; }; + public func getMaxValueSize() : Nat32 { max_value_size_; }; + + /// Saves the node to memory. + public func save(memory: Memory) { + switch(node_type_) { + case(#Leaf){ + if (children_.size() != 0){ + Debug.trap("A leaf node cannot have children."); + }; + }; + case(#Internal){ + if (children_.size() != entries_.size() + 1){ + Debug.trap("An internal node shall have its number of children equal to its number of entries + 1."); + }; + }; + }; + + // We should never be saving an empty node. + if ((entries_.size() == 0) and (children_.size() == 0)){ + Debug.trap("An empty node cannot be saved."); + }; + + // Assert entries are sorted in strictly increasing order. + if (not Utils.isSortedInIncreasingOrder(getKeys(), compareEntryKeys)) { + Debug.trap("The node entries are not sorted in increasing order."); + }; + + let header = { + magic = Blob.toArray(Text.encodeUtf8(MAGIC)); + version = LAYOUT_VERSION; + node_type = switch(node_type_){ + case(#Leaf) { LEAF_NODE_TYPE; }; + case(#Internal) { INTERNAL_NODE_TYPE; }; + }; + num_entries = Nat16.fromNat(entries_.size()); + }; + + saveNodeHeader(header, address_, memory); + + var offset = SIZE_NODE_HEADER; + + // Write the entries. + for ((key, value) in entries_.vals()) { + // Write the size of the key. + Memory.write(memory, address_ + offset, Conversion.nat32ToBytes(Nat32.fromNat(key.size()))); + offset += Nat64.fromNat(U32_SIZE); + + // Write the key. + Memory.write(memory, address_ + offset, key); + offset += Nat64.fromNat(Nat32.toNat(max_key_size_)); + + // Write the size of the value. + Memory.write(memory, address_ + offset, Conversion.nat32ToBytes(Nat32.fromNat(value.size()))); + offset += Nat64.fromNat(U32_SIZE); + + // Write the value. + Memory.write(memory, address_ + offset, value); + offset += Nat64.fromNat(Nat32.toNat(max_value_size_)); + }; + + // Write the children + for (child in children_.vals()){ + Memory.write(memory, address_ + offset, Conversion.nat64ToBytes(child)); + offset += Nat64.fromNat(ADDRESS_SIZE); // Address size + }; + }; + + /// Returns the entry with the max key in the subtree. + public func getMax(memory: Memory) : Entry { + switch(node_type_){ + case(#Leaf) { + // NOTE: a node can never be empty, so this access is safe. + if (entries_.size() == 0) { Debug.trap("A node can never be empty."); }; + entries_.get(entries_.size() - 1); + }; + case(#Internal) { + // NOTE: an internal node must have children, so this access is safe. + if (children_.size() == 0) { Debug.trap("An internal node must have children."); }; + let last_child = load(children_.get(children_.size() - 1), memory, max_key_size_, max_value_size_); + last_child.getMax(memory); + }; + }; + }; + + /// Returns the entry with min key in the subtree. + public func getMin(memory: Memory) : Entry { + switch(node_type_){ + case(#Leaf) { + // NOTE: a node can never be empty, so this access is safe. + if (entries_.size() == 0) { Debug.trap("A node can never be empty."); }; + entries_.get(0); + }; + case(#Internal) { + // NOTE: an internal node must have children, so this access is safe. + if (children_.size() == 0) { Debug.trap("An internal node must have children."); }; + let first_child = load(children_.get(0), memory, max_key_size_, max_value_size_); + first_child.getMin(memory); + }; + }; + }; + + /// Returns true if the node cannot store anymore entries, false otherwise. + public func isFull() : Bool { + entries_.size() >= Nat64.toNat(getCapacity()); + }; + + /// Swaps the entry at index `idx` with the given entry, returning the old entry. + public func swapEntry(idx: Nat, entry: Entry) : Entry { + let old_entry = entries_.get(idx); + entries_.put(idx, entry); + old_entry; + }; + + /// Searches for the key in the node's entries. + /// + /// If the key is found then `Result::Ok` is returned, containing the index + /// of the matching key. If the value is not found then `Result::Err` is + /// returned, containing the index where a matching key could be inserted + /// while maintaining sorted order. + public func getKeyIdx(key: [Nat8]) : Result { + Utils.binarySearch(getKeys(), compareEntryKeys, key); + }; + + /// Get the child at the given index. Traps if the index is superior than the number of children. + public func getChild(idx: Nat) : Address { + children_.get(idx); + }; + + /// Get the entry at the given index. Traps if the index is superior than the number of entries. + public func getEntry(idx: Nat) : Entry { + entries_.get(idx); + }; + + /// Set the node's children + public func setChildren(children: Buffer
) { + children_ := children; + }; + + /// Set the node's entries + public func setEntries(entries: Buffer) { + entries_ := entries; + }; + + /// Set the node's address + public func setAddress(address: Address) { + address_ := address; + }; + + /// Add a child at the end of the node's children. + public func addChild(child: Address) { + children_.add(child); + }; + + /// Add an entry at the end of the node's entries. + public func addEntry(entry: Entry) { + entries_.add(entry); + }; + + /// Remove the child at the end of the node's children. + public func popChild() : ?Address { + children_.removeLast(); + }; + + /// Remove the entry at the end of the node's entries. + public func popEntry() : ?Entry { + entries_.removeLast(); + }; + + /// Insert a child into the node's children at the given index. + public func insertChild(idx: Nat, child: Address) { + Utils.insert(children_, idx, child); + }; + + /// Insert an entry into the node's entries at the given index. + public func insertEntry(idx: Nat, entry: Entry) { + Utils.insert(entries_, idx, entry); + }; + + /// Remove the child from the node's children at the given index. + public func removeChild(idx: Nat) : Address { + Utils.remove(children_, idx); + }; + + /// Remove the entry from the node's entries at the given index. + public func removeEntry(idx: Nat) : Entry { + Utils.remove(entries_, idx); + }; + + /// Append the given children to the node's children + public func appendChildren(children: Buffer
) { + children_.append(children); + }; + + /// Append the given entries to the node's entries + public func appendEntries(entries: Buffer) { + entries_.append(entries); + }; + + func getKeys() : [[Nat8]] { + Array.map(entries_.toArray(), func(entry: Entry) : [Nat8] { entry.0; }); + }; + + public func entriesToText() : Text { + let text_buffer = Buffer.Buffer(0); + text_buffer.add("Entries = ["); + for ((key, val) in entries_.vals()){ + text_buffer.add("e(["); + for (byte in Array.vals(key)){ + text_buffer.add(Nat8.toText(byte) # " "); + }; + text_buffer.add("], ["); + for (byte in Array.vals(val)){ + text_buffer.add(Nat8.toText(byte) # " "); + }; + text_buffer.add("]), "); + }; + text_buffer.add("]"); + Text.join("", text_buffer.vals()); + }; + + }; + + public func makeEntry(key: [Nat8], value: [Nat8]) : Entry { + (key, value); + }; + + /// Compare the two entries using their keys + public func compareEntryKeys(key_a: [Nat8], key_b: [Nat8]) : Order { + Utils.lexicographicallyCompare(key_a, key_b, Nat8.compare); + }; + + /// Deduce the node type based on the node header + func getNodeType(header: NodeHeader) : NodeType { + if (header.node_type == LEAF_NODE_TYPE) { return #Leaf; }; + if (header.node_type == INTERNAL_NODE_TYPE) { return #Internal; }; + Debug.trap("Unknown node type " # Nat8.toText(header.node_type)); + }; + + /// The maximum number of entries per node. + public func getCapacity() : Nat64 { + 2 * Nat64.fromNat(Constants.B) - 1; + }; + + // A transient data structure for reading/writing metadata into/from stable memory. + type NodeHeader = { + magic: [Nat8]; // 3 bytes + version: Nat8; + node_type: Nat8; + num_entries: Nat16; + }; + + let SIZE_NODE_HEADER : Nat64 = 7; + + func saveNodeHeader(header: NodeHeader, addr: Address, memory: Memory) { + Memory.write(memory, addr, header.magic); + Memory.write(memory, addr + 3, [header.version]); + Memory.write(memory, addr + 3 + 1, [header.node_type]); + Memory.write(memory, addr + 3 + 1 + 1, Conversion.nat16ToBytes(header.num_entries)); + }; + + func loadNodeHeader(addr: Address, memory: Memory) : NodeHeader { + let header = { + magic = Memory.read(memory, addr, 3); + version = Memory.read(memory, addr + 3, 1)[0]; + node_type = Memory.read(memory, addr + 3 + 1, 1)[0]; + num_entries = Conversion.bytesToNat16(Memory.read(memory, addr + 3 + 1 + 1, 2)); + }; + header; + }; + +}; \ No newline at end of file diff --git a/src/StableBTree/types.mo b/src/StableBTree/types.mo new file mode 100644 index 00000000..249da448 --- /dev/null +++ b/src/StableBTree/types.mo @@ -0,0 +1,115 @@ +import Result "mo:base/Result"; +import Buffer "mo:base/Buffer"; + +module { + + // For convenience: from base module + type Result = Result.Result; + type Buffer = Buffer.Buffer; + + public type Address = Nat64; + public type Bytes = Nat64; + + public type BytesConverter = { + fromBytes: ([Nat8]) -> T; + toBytes: (T) -> [Nat8]; + }; + + public type Memory = { + size: () -> Nat64; + grow: (Nat64) -> Int64; + write: (Nat64, [Nat8]) -> (); + read: (Nat64, Nat) -> [Nat8]; + }; + + /// An indicator of the current position in the map. + public type Cursor = { + #Address: Address; + #Node: { node: INode; next: Index; }; + }; + + /// An index into a node's child or entry. + public type Index = { + #Child: Nat64; + #Entry: Nat64; + }; + + public type IIter = { + next: () -> ?(K, V); + }; + + // Entries in the node are key-value pairs and both are blobs. + public type Entry = ([Nat8], [Nat8]); + + public type NodeType = { + #Leaf; + #Internal; + }; + + public type INode = { + getAddress: () -> Address; + getEntries: () -> Buffer; + getChildren: () -> Buffer
; + getNodeType: () -> NodeType; + getMaxKeySize: () -> Nat32; + getMaxValueSize: () -> Nat32; + save: (Memory) -> (); + getMax: (Memory) -> Entry; + getMin: (Memory) -> Entry; + isFull: () -> Bool; + swapEntry: (Nat, Entry) -> Entry; + getKeyIdx: ([Nat8]) -> Result; + getChild: (Nat) -> Address; + getEntry: (Nat) -> Entry; + setChildren: (Buffer
) -> (); + setEntries: (Buffer) -> (); + setAddress: (Address) -> (); + addChild: (Address) -> (); + addEntry: (Entry) -> (); + popEntry: () -> ?Entry; + popChild: () -> ?Address; + insertChild: (Nat, Address) -> (); + insertEntry: (Nat, Entry) -> (); + removeChild: (Nat) -> Address; + removeEntry: (Nat) -> Entry; + appendChildren: (Buffer
) -> (); + appendEntries: (Buffer) -> (); + entriesToText: () -> Text; + }; + + public type IAllocator = { + getHeaderAddr: () -> Address; + getAllocationSize: () -> Bytes; + getNumAllocatedChunks: () -> Nat64; + getFreeListHead: () -> Address; + getMemory: () -> Memory; + allocate: () -> Address; + deallocate: (Address) -> (); + saveAllocator: () -> (); + chunkSize: () -> Bytes; + }; + + public type InsertError = { + #KeyTooLarge : { given : Nat; max : Nat; }; + #ValueTooLarge : { given : Nat; max : Nat; }; + }; + + public type IBTreeMap = { + getRootAddr : () -> Address; + getMaxKeySize : () -> Nat32; + getMaxValueSize : () -> Nat32; + getKeyConverter : () -> BytesConverter; + getValueConverter : () -> BytesConverter; + getAllocator : () -> IAllocator; + getLength : () -> Nat64; + getMemory : () -> Memory; + insert : (k: K, v: V) -> Result; + get : (key: K) -> ?V; + containsKey : (key: K) -> Bool; + isEmpty : () -> Bool; + remove : (key: K) -> ?V; + iter : () -> IIter; + loadNode : (address: Address) -> INode; + }; + +}; \ No newline at end of file diff --git a/src/StableBTree/utils.mo b/src/StableBTree/utils.mo new file mode 100644 index 00000000..1a2ba382 --- /dev/null +++ b/src/StableBTree/utils.mo @@ -0,0 +1,186 @@ +import Buffer "mo:base/Buffer"; +import Result "mo:base/Result"; +import List "mo:base/List"; +import Debug "mo:base/Debug"; +import Order "mo:base/Order"; +import Int "mo:base/Int"; + +module { + + // For convenience: from base module + type Buffer = Buffer.Buffer; + type Order = Order.Order; + type Result = Result.Result; + + /// Creates a buffer from an array + public func toBuffer(x :[T]) : Buffer{ + let thisBuffer = Buffer.Buffer(x.size()); + for(thisItem in x.vals()){ + thisBuffer.add(thisItem); + }; + return thisBuffer; + }; + + /// Append two arrays using a buffer + public func append(left: [T], right: [T]) : [T] { + let buffer = Buffer.Buffer(left.size()); + for(val in left.vals()){ + buffer.add(val); + }; + for(val in right.vals()){ + buffer.add(val); + }; + return buffer.toArray(); + }; + + /// Splits the buffers into two at the given index. + /// The right buffer contains the element at the given index + /// similarly to the Rust's vec::split_off method + public func splitOff(buffer: Buffer, idx: Nat) : Buffer{ + var tail = List.nil(); + while(buffer.size() > idx){ + switch(buffer.removeLast()){ + case(null) { assert(false); }; + case(?last){ + tail := List.push(last, tail); + }; + }; + }; + toBuffer(List.toArray(tail)); + }; + + /// Insert an element into the buffer at given index + public func insert(buffer: Buffer, idx: Nat, elem: T) { + let tail = splitOff(buffer, idx); + buffer.add(elem); + buffer.append(tail); + }; + + /// Remove an element from the buffer at the given index + /// Traps if index is out of bounds. + public func remove(buffer: Buffer, idx: Nat) : T { + let tail = splitOff(buffer, idx + 1); + switch(buffer.removeLast()){ + case(null) { Debug.trap("Index is out of bounds."); }; + case(?elem) { + buffer.append(tail); + elem; + }; + }; + }; + + /// Searches the element in the ordered array. + public func binarySearch(array: [T], order: (T, T) -> Order, elem: T) : Result { + // Return index 0 if array is empty + if (array.size() == 0){ + return #err(0); + }; + // Initialize search from first to last index + var left : Nat = 0; + var right : Int = array.size() - 1; // Right can become less than 0, hence the integer type + // Search the array + while (left < right) { + let middle = Int.abs(left + (right - left) / 2); + switch(order(elem, array[middle])){ + // If the element is present at the middle itself + case(#equal) { return #ok(middle); }; + // If element is greater than mid, it can only be present in left subarray + case(#greater) { left := middle + 1; }; + // If element is smaller than mid, it can only be present in right subarray + case(#less) { right := middle - 1; }; + }; + }; + // The search did not find a match + switch(order(elem, array[left])){ + case(#equal) { return #ok(left); }; + case(#greater) { return #err(left + 1); }; + case(#less) { return #err(left); }; + }; + }; + + /// *Copied from the motoko-base library* + /// + /// Defines comparison for two arrays, using `compare` to recursively compare elements in the + /// arrays. Comparison is defined lexicographically. + /// + /// Runtime: O(size) + /// + /// Space: O(1) + /// + /// *Runtime and space assumes that `compare` runs in O(1) time and space. + public func lexicographicallyCompare(array1 : [X], array2 : [X], compare : (X, X) -> Order.Order) : Order.Order { + let size1 = array1.size(); + let size2 = array2.size(); + let minSize = if (size1 < size2) { size1 } else { size2 }; + + var i = 0; + while (i < minSize) { + switch (compare(array1[i], array2[i])) { + case (#less) { + return #less; + }; + case (#greater) { + return #greater; + }; + case _ {}; + }; + i += 1; + }; + + if (size1 < size2) { + #less; + } else if (size1 == size2) { + #equal; + } else { + #greater; + }; + }; + + /// *Copied from the motoko-base library* + /// + /// Checks if `prefix` is a prefix of `array`. Uses `equal` to + /// compare elements. + /// + /// Runtime: O(size of prefix) + /// + /// Space: O(size of prefix) + /// + /// *Runtime and space assumes that `equal` runs in O(1) time and space. + public func isPrefixOf(prefix : [X], array : [X], equal : (X, X) -> Bool) : Bool { + let sizePrefix = prefix.size(); + if (array.size() < sizePrefix) { + return false; + }; + + var i = 0; + while (i < sizePrefix) { + if (not equal(array[i], prefix[i])) { + return false; + }; + + i += 1; + }; + + return true; + }; + + /// Check if the array is sorted in increasing order. + public func isSortedInIncreasingOrder(array: [T], order: (T, T) -> Order) : Bool { + let size_array = array.size(); + var idx : Nat = 0; + // Iterate on the array + while (idx + 1 < size_array){ + switch(order(array[idx], array[idx + 1])){ + case(#greater) { + // Previous is greater than next, wrong order + return false; + }; + case(_) {}; // Previous is less or equal than next, continue iterating + }; + idx += 1; + }; + // All elements have been checked one to one, the array is sorted. + return true; + }; + +}; \ No newline at end of file diff --git a/test/Makefile b/test/Makefile index 92870d45..2ee876cf 100644 --- a/test/Makefile +++ b/test/Makefile @@ -4,7 +4,7 @@ WASMTIME_OPTIONS = --disable-cache OUTDIR=_out -TESTS = $(wildcard *.mo) +TESTS = $(wildcard *.mo) # FIXME somehow add StableBTreeTest into suite TEST_TARGETS = $(patsubst %.mo,_out/%.checked,$(TESTS)) diff --git a/test/stableBTreeTest/integration/Makefile b/test/stableBTreeTest/integration/Makefile new file mode 100755 index 00000000..69a7aa92 --- /dev/null +++ b/test/stableBTreeTest/integration/Makefile @@ -0,0 +1,9 @@ +default: + dfx stop + dfx start --clean --background + dfx deploy singleBTree --argument='(record {max_key_size=32; max_value_size=128})' + ic-repl testSingle.sh + npm install + node testSingle.js + dfx deploy multipleBTrees --argument='(record {max_key_size=32; max_value_size=128})' + ic-repl testMultiple.sh \ No newline at end of file diff --git a/test/stableBTreeTest/integration/dfx.json b/test/stableBTreeTest/integration/dfx.json new file mode 100644 index 00000000..8b62c167 --- /dev/null +++ b/test/stableBTreeTest/integration/dfx.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "dfx": "0.11.2", + "canisters": { + "singleBTree": { + "type": "motoko", + "main": "src/singleBTree.mo" + }, + "multipleBTrees": { + "type": "motoko", + "main": "src/multipleBTrees.mo" + } + }, + "defaults": { + "replica": { + "subnet_type": "system" + } + }, + "networks": { + "local": { + "bind": "127.0.0.1:4943", + "type": "ephemeral" + } + } +} diff --git a/test/stableBTreeTest/integration/package-lock.json b/test/stableBTreeTest/integration/package-lock.json new file mode 100644 index 00000000..f7d792ce --- /dev/null +++ b/test/stableBTreeTest/integration/package-lock.json @@ -0,0 +1,2348 @@ +{ + "name": "integration", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@dfinity/agent": "^0.14.0", + "node-fetch": "^3.2.10", + "tape": "^5.6.1" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dfinity/agent": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.14.0.tgz", + "integrity": "sha512-TjOtIC1tmRX7qFh7+LIg4RpPd6dqkqBfUKCfGYpLr61o4ck2FgjTBJYoKoDIEOJcHIlXruddg7i4zLUiacAtJQ==", + "dependencies": { + "base64-arraybuffer": "^0.2.0", + "bignumber.js": "^9.0.0", + "borc": "^2.1.1", + "js-sha256": "0.9.0", + "simple-cbor": "^0.4.1", + "ts-node": "^10.8.2" + }, + "peerDependencies": { + "@dfinity/candid": "^0.14.0", + "@dfinity/principal": "^0.14.0" + } + }, + "node_modules/@dfinity/candid": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.14.0.tgz", + "integrity": "sha512-eIpt41EpffAY9gCtsSeknBQNMIakawXO24A7RWJ/4m6J9qp18vO3nmdkC2BDEFBRtXGRCIr18FWkvFx6jFiWAw==", + "peer": true, + "dependencies": { + "ts-node": "^10.8.2" + } + }, + "node_modules/@dfinity/principal": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.14.0.tgz", + "integrity": "sha512-/wBjb+QsI4wx3XmS1BnY4ADMHqut5st3prUwpQfMy6dWigm3FyABR1/EKoe+Lrs39WKzWR+QeJyzuUBKEd7buA==", + "peer": true, + "dependencies": { + "ts-node": "^10.8.2" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==" + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "peer": true + }, + "node_modules/acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/array.prototype.every": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/array.prototype.every/-/array.prototype.every-1.1.3.tgz", + "integrity": "sha512-vWnriJI//SOMOWtXbU/VXhJ/InfnNHPF6BLKn5WfY8xXy+NWql0fUy20GO3sdqBhCAO+qw8S/E5nJiZX+QFdCA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-arraybuffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", + "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", + "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==", + "engines": { + "node": "*" + } + }, + "node_modules/borc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/borc/-/borc-2.1.2.tgz", + "integrity": "sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w==", + "dependencies": { + "bignumber.js": "^9.0.0", + "buffer": "^5.5.0", + "commander": "^2.15.0", + "ieee754": "^1.1.13", + "iso-url": "~0.4.7", + "json-text-sequence": "~0.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/deep-equal": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", + "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "dependencies": { + "call-bind": "^1.0.0", + "es-get-iterator": "^1.1.1", + "get-intrinsic": "^1.0.1", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.2", + "is-regex": "^1.1.1", + "isarray": "^2.0.5", + "object-is": "^1.1.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3", + "which-boxed-primitive": "^1.0.1", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delimit-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz", + "integrity": "sha512-a02fiQ7poS5CnjiJBAsjGLPp5EwVoGHNeu9sziBd9huppRfsAFIpv5zNLv0V1gbop53ilngAf5Kf331AwcoRBQ==" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotignore": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", + "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "dependencies": { + "minimatch": "^3.0.4" + }, + "bin": { + "ignored": "bin/ignored" + } + }, + "node_modules/es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", + "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-dynamic-import": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-dynamic-import/-/has-dynamic-import-2.0.1.tgz", + "integrity": "sha512-X3fbtsZmwb6W7fJGR9o7x65fZoodygCrZ3TVycvghP62yYQfS0t4RS0Qcz+j5tQYUKeSWS09tHkWW6WhFV3XhQ==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", + "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/iso-url": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/iso-url/-/iso-url-0.4.7.tgz", + "integrity": "sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "node_modules/json-text-sequence": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz", + "integrity": "sha512-L3mEegEWHRekSHjc7+sc8eJhba9Clq1PZ8kMkzf8OxElhXc8O4TS5MwcVlj9aEbm5dr81N90WHC5nAz3UO971w==", + "dependencies": { + "delimit-stream": "0.1.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz", + "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha512-Fn9X8rX8yYF4m81rZCK/5VmrmsSbqS/i3rDLl6ZZHAXgC2nTAx3dhwG8q8odP/RmdLa2YrybDJaAMg+X1ajY3w==", + "dependencies": { + "through": "~2.3.4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-cbor": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/simple-cbor/-/simple-cbor-0.4.1.tgz", + "integrity": "sha512-rijcxtwx2b4Bje3sqeIqw5EeW7UlOIC4YfOdwqIKacpvRQ/D78bWg/4/0m5e0U91oKvlGh7LlJuZCu07ISCC7w==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.6.tgz", + "integrity": "sha512-8lMR2m+U0VJTPp6JjvJTtGyc4FIGq9CdRt7O9p6T0e6K4vjU+OP+SQJpbe/SBmRcCUIvNUnjsbmY6lnMp8MhsQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tape": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/tape/-/tape-5.6.1.tgz", + "integrity": "sha512-reNzS3rzsJtKk0f+zJx2XlzIsjJXlIcOIrIxk5shHAG/DzW3BKyMg8UfN79oluYlcWo4lIt56ahLqwgpRT4idg==", + "dependencies": { + "array.prototype.every": "^1.1.3", + "call-bind": "^1.0.2", + "deep-equal": "^2.0.5", + "defined": "^1.0.0", + "dotignore": "^0.1.2", + "for-each": "^0.3.3", + "get-package-type": "^0.1.0", + "glob": "^7.2.3", + "has": "^1.0.3", + "has-dynamic-import": "^2.0.1", + "inherits": "^2.0.4", + "is-regex": "^1.1.4", + "minimist": "^1.2.6", + "object-inspect": "^1.12.2", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "resolve": "^2.0.0-next.3", + "resumer": "^0.0.0", + "string.prototype.trim": "^1.2.6", + "through": "^2.3.8" + }, + "bin": { + "tape": "bin/tape" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", + "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@dfinity/agent": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.14.0.tgz", + "integrity": "sha512-TjOtIC1tmRX7qFh7+LIg4RpPd6dqkqBfUKCfGYpLr61o4ck2FgjTBJYoKoDIEOJcHIlXruddg7i4zLUiacAtJQ==", + "requires": { + "base64-arraybuffer": "^0.2.0", + "bignumber.js": "^9.0.0", + "borc": "^2.1.1", + "js-sha256": "0.9.0", + "simple-cbor": "^0.4.1", + "ts-node": "^10.8.2" + } + }, + "@dfinity/candid": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.14.0.tgz", + "integrity": "sha512-eIpt41EpffAY9gCtsSeknBQNMIakawXO24A7RWJ/4m6J9qp18vO3nmdkC2BDEFBRtXGRCIr18FWkvFx6jFiWAw==", + "peer": true, + "requires": { + "ts-node": "^10.8.2" + } + }, + "@dfinity/principal": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.14.0.tgz", + "integrity": "sha512-/wBjb+QsI4wx3XmS1BnY4ADMHqut5st3prUwpQfMy6dWigm3FyABR1/EKoe+Lrs39WKzWR+QeJyzuUBKEd7buA==", + "peer": true, + "requires": { + "ts-node": "^10.8.2" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==" + }, + "@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "peer": true + }, + "acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "array.prototype.every": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/array.prototype.every/-/array.prototype.every-1.1.3.tgz", + "integrity": "sha512-vWnriJI//SOMOWtXbU/VXhJ/InfnNHPF6BLKn5WfY8xXy+NWql0fUy20GO3sdqBhCAO+qw8S/E5nJiZX+QFdCA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "is-string": "^1.0.7" + } + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-arraybuffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", + "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bignumber.js": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", + "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==" + }, + "borc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/borc/-/borc-2.1.2.tgz", + "integrity": "sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w==", + "requires": { + "bignumber.js": "^9.0.0", + "buffer": "^5.5.0", + "commander": "^2.15.0", + "ieee754": "^1.1.13", + "iso-url": "~0.4.7", + "json-text-sequence": "~0.1.0", + "readable-stream": "^3.6.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==" + }, + "deep-equal": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", + "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "requires": { + "call-bind": "^1.0.0", + "es-get-iterator": "^1.1.1", + "get-intrinsic": "^1.0.1", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.2", + "is-regex": "^1.1.1", + "isarray": "^2.0.5", + "object-is": "^1.1.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3", + "which-boxed-primitive": "^1.0.1", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.2" + } + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==" + }, + "delimit-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz", + "integrity": "sha512-a02fiQ7poS5CnjiJBAsjGLPp5EwVoGHNeu9sziBd9huppRfsAFIpv5zNLv0V1gbop53ilngAf5Kf331AwcoRBQ==" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "dotignore": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", + "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-get-iterator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", + "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "has-dynamic-import": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-dynamic-import/-/has-dynamic-import-2.0.1.tgz", + "integrity": "sha512-X3fbtsZmwb6W7fJGR9o7x65fZoodygCrZ3TVycvghP62yYQfS0t4RS0Qcz+j5tQYUKeSWS09tHkWW6WhFV3XhQ==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==" + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==" + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", + "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0" + } + }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==" + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "iso-url": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/iso-url/-/iso-url-0.4.7.tgz", + "integrity": "sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==" + }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "json-text-sequence": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz", + "integrity": "sha512-L3mEegEWHRekSHjc7+sc8eJhba9Clq1PZ8kMkzf8OxElhXc8O4TS5MwcVlj9aEbm5dr81N90WHC5nAz3UO971w==", + "requires": { + "delimit-stream": "0.1.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" + }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, + "node-fetch": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz", + "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha512-Fn9X8rX8yYF4m81rZCK/5VmrmsSbqS/i3rDLl6ZZHAXgC2nTAx3dhwG8q8odP/RmdLa2YrybDJaAMg+X1ajY3w==", + "requires": { + "through": "~2.3.4" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "simple-cbor": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/simple-cbor/-/simple-cbor-0.4.1.tgz", + "integrity": "sha512-rijcxtwx2b4Bje3sqeIqw5EeW7UlOIC4YfOdwqIKacpvRQ/D78bWg/4/0m5e0U91oKvlGh7LlJuZCu07ISCC7w==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string.prototype.trim": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.6.tgz", + "integrity": "sha512-8lMR2m+U0VJTPp6JjvJTtGyc4FIGq9CdRt7O9p6T0e6K4vjU+OP+SQJpbe/SBmRcCUIvNUnjsbmY6lnMp8MhsQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "tape": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/tape/-/tape-5.6.1.tgz", + "integrity": "sha512-reNzS3rzsJtKk0f+zJx2XlzIsjJXlIcOIrIxk5shHAG/DzW3BKyMg8UfN79oluYlcWo4lIt56ahLqwgpRT4idg==", + "requires": { + "array.prototype.every": "^1.1.3", + "call-bind": "^1.0.2", + "deep-equal": "^2.0.5", + "defined": "^1.0.0", + "dotignore": "^0.1.2", + "for-each": "^0.3.3", + "get-package-type": "^0.1.0", + "glob": "^7.2.3", + "has": "^1.0.3", + "has-dynamic-import": "^2.0.1", + "inherits": "^2.0.4", + "is-regex": "^1.1.4", + "minimist": "^1.2.6", + "object-inspect": "^1.12.2", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "resolve": "^2.0.0-next.3", + "resumer": "^0.0.0", + "string.prototype.trim": "^1.2.6", + "through": "^2.3.8" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "peer": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-typed-array": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", + "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.9" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + } + } +} diff --git a/test/stableBTreeTest/integration/package.json b/test/stableBTreeTest/integration/package.json new file mode 100644 index 00000000..1b54b8bf --- /dev/null +++ b/test/stableBTreeTest/integration/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "dependencies": { + "@dfinity/agent": "^0.14.0", + "node-fetch": "^3.2.10", + "tape": "^5.6.1" + } +} diff --git a/test/stableBTreeTest/integration/src/multipleBTrees.mo b/test/stableBTreeTest/integration/src/multipleBTrees.mo new file mode 100644 index 00000000..7e783df4 --- /dev/null +++ b/test/stableBTreeTest/integration/src/multipleBTrees.mo @@ -0,0 +1,154 @@ +import StableBTree "../../../src/btreemap"; +import StableBTreeTypes "../../../src/types"; +import Conversion "../../../src/conversion"; +import Memory "../../../src/memory"; +import MemoryManager "../../../src/memoryManager"; + +import Result "mo:base/Result"; +import Array "mo:base/Array"; +import Buffer "mo:base/Buffer"; +import Iter "mo:base/Iter"; +import Nat8 "mo:base/Nat8"; +import Nat32 "mo:base/Nat32"; +import TrieSet "mo:base/TrieSet"; +import Trie "mo:base/Trie"; + +actor class MultipleBTrees(args: { + max_key_size: Nat32; + max_value_size: Nat32; +}) { + + // For convenience: from StableBTree types + type Address = StableBTreeTypes.Address; + type BytesConverter = StableBTreeTypes.BytesConverter; + type Memory = StableBTreeTypes.Memory; + type InsertError = StableBTreeTypes.InsertError; + type MemoryId = MemoryManager.MemoryId; + + // For convenience: from base module + type Result = Result.Result; + type Set = TrieSet.Set; + + // Arbitrary use of (Nat32, Text) for (key, value) types + type K = Nat32; + type V = Text; + + let nat32_converter_ = { + fromBytes = func(bytes: [Nat8]) : Nat32 { Conversion.bytesToNat32(bytes); }; + toBytes = func(nat32: Nat32) : [Nat8] { Conversion.nat32ToBytes(nat32); }; + }; + + let text_converter_ = { + fromBytes = func(bytes: [Nat8]) : Text { Conversion.bytesToText(bytes); }; + toBytes = func(text: Text) : [Nat8] { Conversion.textToBytes(text); }; + }; + + // The memory manager + let memory_manager_ = MemoryManager.init(Memory.STABLE_MEMORY); + + // The BTreeMap identifiers + var identifiers_ = TrieSet.empty(); + + // Get or create the BTreeMap identified with the btree_id + func getBTreeMap(btree_id: MemoryId) : StableBTree.BTreeMap { + let memory = memory_manager_.get(btree_id); + switch(Trie.get(identifiers_, { key = btree_id; hash = Nat32.fromNat(Nat8.toNat(btree_id)); }, Nat8.equal)){ + case(null){ + identifiers_ := TrieSet.put(identifiers_, btree_id, Nat32.fromNat(Nat8.toNat(btree_id)), Nat8.equal); + StableBTree.init(memory, args.max_key_size, args.max_value_size, nat32_converter_, text_converter_); + }; + case(_){ + StableBTree.load(memory, nat32_converter_, text_converter_); + }; + }; + }; + + public func getLength(btree_id: MemoryId) : async Nat64 { + let btreemap = getBTreeMap(btree_id); + btreemap.getLength(); + }; + + public func insert(btree_id: MemoryId, key: K, value: V) : async Result { + let btreemap = getBTreeMap(btree_id); + btreemap.insert(key, value); + }; + + public func get(btree_id: MemoryId, key: K) : async ?V { + let btreemap = getBTreeMap(btree_id); + btreemap.get(key); + }; + + public func containsKey(btree_id: MemoryId, key: K) : async Bool { + let btreemap = getBTreeMap(btree_id); + btreemap.containsKey(key); + }; + + public func isEmpty(btree_id: MemoryId) : async Bool { + let btreemap = getBTreeMap(btree_id); + btreemap.isEmpty(); + }; + + public func remove(btree_id: MemoryId, key: K) : async ?V { + let btreemap = getBTreeMap(btree_id); + getBTreeMap(btree_id).remove(key); + }; + + public func insertMany(btree_id: MemoryId, entries: [(K, V)]) : async Result<(), [InsertError]> { + let btreemap = getBTreeMap(btree_id); + let buffer = Buffer.Buffer(0); + for ((key, value) in Array.vals(entries)){ + switch(btreemap.insert(key, value)){ + case(#err(insert_error)) { buffer.add(insert_error); }; + case(_) {}; + }; + }; + if (buffer.size() > 0){ + #err(buffer.toArray()); + } else { + #ok; + }; + }; + + public func getMany(btree_id: MemoryId, keys: [K]) : async [V] { + let btreemap = getBTreeMap(btree_id); + let buffer = Buffer.Buffer(0); + for (key in Array.vals(keys)){ + switch(btreemap.get(key)){ + case(?value) { buffer.add(value); }; + case(null) {}; + }; + }; + buffer.toArray(); + }; + + public func containsKeys(btree_id: MemoryId, keys: [K]) : async Bool { + let btreemap = getBTreeMap(btree_id); + for (key in Array.vals(keys)){ + if (not btreemap.containsKey(key)) { + return false; + }; + }; + return true; + }; + + public func removeMany(btree_id: MemoryId, keys: [K]) : async [V] { + let btreemap = getBTreeMap(btree_id); + let buffer = Buffer.Buffer(0); + for (key in Array.vals(keys)){ + switch(btreemap.remove(key)){ + case(?value) { buffer.add(value); }; + case(null) {}; + }; + }; + buffer.toArray(); + }; + + public func empty(btree_id: MemoryId) : async () { + let btreemap = getBTreeMap(btree_id); + let entries = Iter.toArray(btreemap.iter()); + for ((key, _) in Array.vals(entries)){ + ignore btreemap.remove(key); + }; + }; + +}; diff --git a/test/stableBTreeTest/integration/src/singleBTree.mo b/test/stableBTreeTest/integration/src/singleBTree.mo new file mode 100644 index 00000000..abc7aa84 --- /dev/null +++ b/test/stableBTreeTest/integration/src/singleBTree.mo @@ -0,0 +1,117 @@ +import StableBTree "../../../src/btreemap"; +import StableBTreeTypes "../../../src/types"; +import Conversion "../../../src/conversion"; +import Memory "../../../src/memory"; + +import Result "mo:base/Result"; +import Array "mo:base/Array"; +import Buffer "mo:base/Buffer"; +import Iter "mo:base/Iter"; + +actor class SingleBTree(args: { + max_key_size: Nat32; + max_value_size: Nat32; +}) { + + // For convenience: from StableBTree types + type Address = StableBTreeTypes.Address; + type BytesConverter = StableBTreeTypes.BytesConverter; + type Memory = StableBTreeTypes.Memory; + type InsertError = StableBTreeTypes.InsertError; + // For convenience: from base module + type Result = Result.Result; + + // Arbitrary use of (Nat32, Text) for (key, value) types + type K = Nat32; + type V = Text; + + let nat32_converter_ = { + fromBytes = func(bytes: [Nat8]) : Nat32 { Conversion.bytesToNat32(bytes); }; + toBytes = func(nat32: Nat32) : [Nat8] { Conversion.nat32ToBytes(nat32); }; + }; + + let text_converter_ = { + fromBytes = func(bytes: [Nat8]) : Text { Conversion.bytesToText(bytes); }; + toBytes = func(text: Text) : [Nat8] { Conversion.textToBytes(text); }; + }; + + let btreemap_ = StableBTree.init(Memory.STABLE_MEMORY, args.max_key_size, args.max_value_size, nat32_converter_, text_converter_); + + public func getLength() : async Nat64 { + btreemap_.getLength(); + }; + + public func insert(key: K, value: V) : async Result { + btreemap_.insert(key, value); + }; + + public func get(key: K) : async ?V { + btreemap_.get(key); + }; + + public func containsKey(key: K) : async Bool { + btreemap_.containsKey(key); + }; + + public func isEmpty() : async Bool { + btreemap_.isEmpty(); + }; + + public func remove(key: K) : async ?V { + btreemap_.remove(key); + }; + + public func insertMany(entries: [(K, V)]) : async Result<(), [InsertError]> { + let buffer = Buffer.Buffer(0); + for ((key, value) in Array.vals(entries)){ + switch(btreemap_.insert(key, value)){ + case(#err(insert_error)) { buffer.add(insert_error); }; + case(_) {}; + }; + }; + if (buffer.size() > 0){ + #err(buffer.toArray()); + } else { + #ok; + }; + }; + + public func getMany(keys: [K]) : async [V] { + let buffer = Buffer.Buffer(0); + for (key in Array.vals(keys)){ + switch(btreemap_.get(key)){ + case(?value) { buffer.add(value); }; + case(null) {}; + }; + }; + buffer.toArray(); + }; + + public func containsKeys(keys: [K]) : async Bool { + for (key in Array.vals(keys)){ + if (not btreemap_.containsKey(key)) { + return false; + }; + }; + return true; + }; + + public func removeMany(keys: [K]) : async [V] { + let buffer = Buffer.Buffer(0); + for (key in Array.vals(keys)){ + switch(btreemap_.remove(key)){ + case(?value) { buffer.add(value); }; + case(null) {}; + }; + }; + buffer.toArray(); + }; + + public func empty() : async () { + let entries = Iter.toArray(btreemap_.iter()); + for ((key, _) in Array.vals(entries)){ + ignore btreemap_.remove(key); + }; + }; + +}; diff --git a/test/stableBTreeTest/integration/testMultiple.sh b/test/stableBTreeTest/integration/testMultiple.sh new file mode 100755 index 00000000..ff7134a9 --- /dev/null +++ b/test/stableBTreeTest/integration/testMultiple.sh @@ -0,0 +1,102 @@ +#!/usr/local/bin/ic-repl + +function install(wasm, args) { + import interface = "2vxsx-fae" as ".dfx/local/canisters/multipleBTrees/multipleBTrees.did"; + let id = call ic.provisional_create_canister_with_cycles(record { settings = null; amount = null; }); + call ic.install_code( + record { + arg = encode interface.__init_args(args); + wasm_module = wasm; + mode = variant { install }; + canister_id = id.canister_id; + } + ); + id.canister_id +}; + +function upgrade(canister_id, wasm, args) { + import interface = "2vxsx-fae" as ".dfx/local/canisters/multipleBTrees/multipleBTrees.did"; + call ic.install_code( + record { + arg = encode interface.__init_args(args); + wasm_module = wasm; + mode = variant { upgrade }; + canister_id = canister_id; + } + ); +}; + +function reinstall(canister_id, wasm, args) { + import interface = "2vxsx-fae" as ".dfx/local/canisters/multipleBTrees/multipleBTrees.did"; + call ic.install_code( + record { + arg = encode interface.__init_args(args); + wasm_module = wasm; + mode = variant { reinstall }; + canister_id = canister_id; + } + ); +}; + +let args = record { max_key_size = 32; max_value_size = 128; }; + +// Create the canister +let multiple_btrees = install(file(".dfx/local/canisters/multipleBTrees/multipleBTrees.wasm"), args); + +// Use a first btreemap of identifier 0 +call multiple_btrees.getLength(0); +assert _ == (0 : nat64); +call multiple_btrees.insert(0, 12345, "hello"); +assert _ == variant { ok = null : opt record{} }; +call multiple_btrees.getLength(0); +assert _ == (1 : nat64); +call multiple_btrees.get(0, 12345); +assert _ == opt("hello" : text); + +// Use a second btreemap of identifier 1 +call multiple_btrees.getLength(1); +assert _ == (0 : nat64); +call multiple_btrees.get(1, 12345); +assert _ == (null : opt record{}); +call multiple_btrees.insert(1, 67890, "hi"); +call multiple_btrees.insert(1, 45678, "ola"); +call multiple_btrees.insert(1, 34567, "salut"); +assert _ == variant { ok = null : opt record{} }; +call multiple_btrees.getLength(1); +assert _ == (3 : nat64); +call multiple_btrees.get(1, 67890); +assert _ == opt("hi" : text); +call multiple_btrees.get(1, 45678); +assert _ == opt("ola" : text); +call multiple_btrees.get(1, 34567); +assert _ == opt("salut" : text); + +// Both BTrees shall be preserved after an upgrade +upgrade(multiple_btrees, file(".dfx/local/canisters/multipleBTrees/multipleBTrees.wasm"), args); +call multiple_btrees.getLength(0); +assert _ == (1 : nat64); +call multiple_btrees.get(0, 12345); +assert _ == opt("hello" : text); +call multiple_btrees.getLength(1); +assert _ == (3 : nat64); +call multiple_btrees.get(1, 67890); +assert _ == opt("hi" : text); +call multiple_btrees.get(1, 45678); +assert _ == opt("ola" : text); +call multiple_btrees.get(1, 34567); +assert _ == opt("salut" : text); + +// Both BTrees shall be emptied after a reinstall +reinstall(multiple_btrees, file(".dfx/local/canisters/multipleBTrees/multipleBTrees.wasm"), args); +call multiple_btrees.getLength(0); +assert _ == (0 : nat64); +call multiple_btrees.get(0, 12345); +assert _ == (null : opt record{}); +call multiple_btrees.getLength(1); +assert _ == (0 : nat64); +call multiple_btrees.get(1, 67890); +assert _ == (null : opt record{}); +call multiple_btrees.get(1, 45678); +assert _ == (null : opt record{}); +call multiple_btrees.get(1, 34567); +assert _ == (null : opt record{}); diff --git a/test/stableBTreeTest/integration/testSingle.js b/test/stableBTreeTest/integration/testSingle.js new file mode 100644 index 00000000..4d27ce1a --- /dev/null +++ b/test/stableBTreeTest/integration/testSingle.js @@ -0,0 +1,96 @@ +import { HttpAgent, Actor } from "@dfinity/agent"; +import { idlFactory } from "./.dfx/local/canisters/singleBTree/singleBTree.did.js"; +import fetch from "node-fetch"; +import { test } from "tape"; +// From https://stackoverflow.com/a/74018376/6021706 +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +const canister_ids = require("./.dfx/local/canister_ids.json"); + +global.fetch = fetch; + +const agent = new HttpAgent({ + host: "http://127.0.0.1:4943/" +}); + +agent.fetchRootKey().catch((err) => { + console.warn("Unable to fetch root key. Check to ensure that your local replica is running"); + console.error(err); +}); + +const single_b_tree = Actor.createActor(idlFactory, { + agent: agent, + canisterId: canister_ids["singleBTree"]["local"] +}); + +let NUM_INSERTIONS = 5000; + +test('random_insertions', async function (t) { + // Remove previous entries in the btree if any + await single_b_tree.empty(); + t.equal(await single_b_tree.getLength(), 0n); + + // Insert NUM_INSERTIONS random entries + let random_keys = []; + for (var i=0; i [key, key.toString()]); + + // Verify the insertion works + t.ok(await single_b_tree.insertMany(entries)); + + const unique_keys = [...new Set(random_keys)]; + + // Verify the length of the btree + t.equal(await single_b_tree.getLength(), BigInt(unique_keys.length)); + + // Verify retrieving each value works (use join to compare array's content) + t.equal((await single_b_tree.getMany(unique_keys)).join(""), unique_keys.map(key => key.toString()).join("")); +}); + +test('increasing_insertions', async function (t) { + // Remove previous entries in the btree if any + await single_b_tree.empty(); + t.equal(await single_b_tree.getLength(), 0n); + + // Insert NUM_INSERTIONS increasing entries + let entries = []; + for (var i=0; i entry[0]); + let values = entries.map(entry => entry[1]); + + // Verify the insertion works + t.ok(await single_b_tree.insertMany(entries)); + // Verify the length of the btree + t.equal(await single_b_tree.getLength(), BigInt(entries.length)); + // Verify retrieving each value works (use join to compare array's content) + t.equal((await single_b_tree.getMany(keys)).join(""), values.join("")); +}); + +test('decreasing_insertions', async function (t) { + // Remove previous entries in the btree if any + await single_b_tree.empty(); + t.equal(await single_b_tree.getLength(), 0n); + + // Insert NUM_INSERTIONS decreasing entries + let entries = []; + for (var i=(NUM_INSERTIONS-1); i >= 0; i--){ + entries.push([i, i.toString()]); + }; + let keys = entries.map(entry => entry[0]); + let values = entries.map(entry => entry[1]); + + // Verify the insertion works + t.ok(await single_b_tree.insertMany(entries)); + // Verify the length of the btree + t.equal(await single_b_tree.getLength(), BigInt(entries.length)); + // Verify retrieving each value works (use join to compare array's content) + t.equal((await single_b_tree.getMany(keys)).join(""), values.join("")); +}); \ No newline at end of file diff --git a/test/stableBTreeTest/integration/testSingle.sh b/test/stableBTreeTest/integration/testSingle.sh new file mode 100755 index 00000000..f7674300 --- /dev/null +++ b/test/stableBTreeTest/integration/testSingle.sh @@ -0,0 +1,69 @@ +#!/usr/local/bin/ic-repl + +function install(wasm, args) { + import interface = "2vxsx-fae" as ".dfx/local/canisters/singleBTree/singleBTree.did"; + let id = call ic.provisional_create_canister_with_cycles(record { settings = null; amount = null; }); + call ic.install_code( + record { + arg = encode interface.__init_args(args); + wasm_module = wasm; + mode = variant { install }; + canister_id = id.canister_id; + } + ); + id.canister_id +}; + +function upgrade(canister_id, wasm, args) { + import interface = "2vxsx-fae" as ".dfx/local/canisters/singleBTree/singleBTree.did"; + call ic.install_code( + record { + arg = encode interface.__init_args(args); + wasm_module = wasm; + mode = variant { upgrade }; + canister_id = canister_id; + } + ); +}; + +function reinstall(canister_id, wasm, args) { + import interface = "2vxsx-fae" as ".dfx/local/canisters/singleBTree/singleBTree.did"; + call ic.install_code( + record { + arg = encode interface.__init_args(args); + wasm_module = wasm; + mode = variant { reinstall }; + canister_id = canister_id; + } + ); +}; + +let args = record { max_key_size = 32; max_value_size = 128; }; + +// Create a BTree +let btree_canister = install(file(".dfx/local/canisters/singleBTree/singleBTree.wasm"), args); +// Verify it is empty +call btree_canister.getLength(); +assert _ == (0 : nat64); +// Insert a pair of key/value +call btree_canister.insert(12345, "hello"); +assert _ == variant { ok = null : opt record{} }; +// Verify the Btree contains the pair of key/value +call btree_canister.getLength(); +assert _ == (1 : nat64); +call btree_canister.get(12345); +assert _ == opt("hello" : text); + +// The BTree shall be preserved after an upgrade +upgrade(btree_canister, file(".dfx/local/canisters/singleBTree/singleBTree.wasm"), args); +call btree_canister.getLength(); +assert _ == (1 : nat64); +call btree_canister.get(12345); +assert _ == opt("hello" : text); + +// The BTree shall be empty after a reinstall +reinstall(btree_canister, file(".dfx/local/canisters/singleBTree/singleBTree.wasm"), args); +call btree_canister.getLength(); +assert _ == (0 : nat64); +call btree_canister.get(12345); +assert _ == (null : opt record{}); \ No newline at end of file diff --git a/test/stableBTreeTest/module/Makefile b/test/stableBTreeTest/module/Makefile new file mode 100644 index 00000000..15fdee4e --- /dev/null +++ b/test/stableBTreeTest/module/Makefile @@ -0,0 +1,3 @@ +default: + $(shell vessel bin)/moc $(shell vessel sources) -wasi-system-api -o suite.wasm suite.mo && wasmtime suite.wasm + rm -f suite.wasm diff --git a/test/stableBTreeTest/module/package-set.dhall b/test/stableBTreeTest/module/package-set.dhall new file mode 100644 index 00000000..77119fd9 --- /dev/null +++ b/test/stableBTreeTest/module/package-set.dhall @@ -0,0 +1 @@ +../../package-set.dhall diff --git a/test/stableBTreeTest/module/suite.mo b/test/stableBTreeTest/module/suite.mo new file mode 100644 index 00000000..5dba8458 --- /dev/null +++ b/test/stableBTreeTest/module/suite.mo @@ -0,0 +1,9 @@ +import TestIter "testIter"; +import TestAllocator "testAllocator"; +import TestBTreeMap "testBTreeMap"; +import TestMemoryManager "testMemoryManager"; + +TestAllocator.run(); +TestBTreeMap.run(); +TestIter.run(); +TestMemoryManager.run(); diff --git a/test/stableBTreeTest/module/testAllocator.mo b/test/stableBTreeTest/module/testAllocator.mo new file mode 100644 index 00000000..3bc2a7db --- /dev/null +++ b/test/stableBTreeTest/module/testAllocator.mo @@ -0,0 +1,143 @@ +import Memory "../../src/memory"; +import Allocator "../../src/allocator"; +import Constants "../../src/constants"; +import TestableItems "testableItems"; + +import Nat64 "mo:base/Nat64"; +import Iter "mo:base/Iter"; + +module { + + // For convenience: from base module + type Iter = Iter.Iter; + // For convenience: from other modules + type TestBuffer = TestableItems.TestBuffer; + + func newAndLoad(test: TestBuffer) { + let memory = Memory.VecMemory(); + let allocator_addr = Constants.ADDRESS_0; + let allocation_size : Nat64 = 16; + + // Create a new allocator. + ignore Allocator.initAllocator(memory, allocator_addr, allocation_size); + + // Load it from memory. + let allocator = Allocator.loadAllocator(memory, allocator_addr); + + test.equalsNat64(allocator.getAllocationSize(), allocation_size); + test.equalsNat64(allocator.getFreeListHead(), allocator_addr + Allocator.SIZE_ALLOCATOR_HEADER); + + // Load the first memory chunk. + let chunk = Allocator.loadChunkHeader(allocator.getFreeListHead(), memory); + test.equalsNat64(chunk.next, Constants.NULL); + }; + + func allocate(test: TestBuffer) { + let memory = Memory.VecMemory(); + let allocator_addr = Constants.ADDRESS_0; + let allocation_size : Nat64 = 16; + + let allocator = Allocator.initAllocator(memory, allocator_addr, allocation_size); + + let original_free_list_head = allocator.getFreeListHead(); + + for (i in Iter.range(1, 3)){ + ignore allocator.allocate(); + test.equalsNat64(allocator.getFreeListHead() , original_free_list_head + allocator.chunkSize() * Nat64.fromNat(i)); + }; + }; + + func allocateLarge(test: TestBuffer) { + // Allocate large chunks to verify that we are growing the memory. + let memory = Memory.VecMemory(); + test.equalsNat64(memory.size() , 0); + let allocator_addr = Constants.ADDRESS_0; + let allocation_size = Constants.WASM_PAGE_SIZE; + + var allocator = Allocator.initAllocator(memory, allocator_addr, allocation_size); + test.equalsNat64(memory.size() , 1); + + ignore allocator.allocate(); + test.equalsNat64(memory.size() , 2); + + ignore allocator.allocate(); + test.equalsNat64(memory.size() , 3); + + ignore allocator.allocate(); + test.equalsNat64(memory.size() , 4); + + // Each allocation should push the `head` by `chunk_size`. + test.equalsNat64(allocator.getFreeListHead() , allocator_addr + Allocator.SIZE_ALLOCATOR_HEADER + allocator.chunkSize() * 3); + test.equalsNat64(allocator.getNumAllocatedChunks() , 3); + + // Load and reload to verify that the data is the same. + allocator := Allocator.loadAllocator(memory, Constants.ADDRESS_0); + test.equalsNat64(allocator.getFreeListHead() , allocator_addr + Allocator.SIZE_ALLOCATOR_HEADER + allocator.chunkSize() * 3); + test.equalsNat64(allocator.getNumAllocatedChunks() , 3); + }; + + func allocateThenDeallocate(test: TestBuffer) { + let memory = Memory.VecMemory(); + let allocation_size : Nat64 = 16; + let allocator_addr = Constants.ADDRESS_0; + var allocator = Allocator.initAllocator(memory, allocator_addr, allocation_size); + + let chunk_addr = allocator.allocate(); + test.equalsNat64(allocator.getFreeListHead() , allocator_addr + Allocator.SIZE_ALLOCATOR_HEADER + allocator.chunkSize()); + + allocator.deallocate(chunk_addr); + test.equalsNat64(allocator.getFreeListHead() , allocator_addr + Allocator.SIZE_ALLOCATOR_HEADER); + test.equalsNat64(allocator.getNumAllocatedChunks() , 0); + + // Load and reload to verify that the data is the same. + allocator := Allocator.loadAllocator(memory, allocator_addr); + test.equalsNat64(allocator.getFreeListHead() , allocator_addr + Allocator.SIZE_ALLOCATOR_HEADER); + test.equalsNat64(allocator.getNumAllocatedChunks() , 0); + }; + + func allocateThenDeallocate2(test: TestBuffer) { + let memory = Memory.VecMemory(); + let allocation_size : Nat64 = 16; + let allocator_addr = Constants.ADDRESS_0; + var allocator = Allocator.initAllocator(memory, allocator_addr, allocation_size); + + ignore allocator.allocate(); + let chunk_addr_2 = allocator.allocate(); + test.equalsNat64(allocator.getFreeListHead() , chunk_addr_2 + allocation_size); + + allocator.deallocate(chunk_addr_2); + test.equalsNat64(allocator.getFreeListHead() , chunk_addr_2 - Allocator.SIZE_CHUNK_HEADER); + + let chunk_addr_3 = allocator.allocate(); + test.equalsNat64(chunk_addr_3 , chunk_addr_2); + test.equalsNat64(allocator.getFreeListHead() , chunk_addr_3 + allocation_size); + }; + + func deallocateFreeChunk(test: TestBuffer) { + let memory = Memory.VecMemory(); + let allocation_size : Nat64 = 16; + let allocator_addr = Constants.ADDRESS_0; + let allocator = Allocator.initAllocator(memory, allocator_addr, allocation_size); + + let chunk_addr = allocator.allocate(); + allocator.deallocate(chunk_addr); + + // Try deallocating the free chunk - should trap. + // @todo: succeed on trap + //allocator.deallocate(chunk_addr); + }; + + public func run() { + let test = TestableItems.TestBuffer(); + + newAndLoad(test); + allocate(test); + allocateLarge(test); + allocateThenDeallocate(test); + allocateThenDeallocate2(test); + deallocateFreeChunk(test); + + test.run("Test allocator module"); + }; + +}; diff --git a/test/stableBTreeTest/module/testBTreeMap.mo b/test/stableBTreeTest/module/testBTreeMap.mo new file mode 100644 index 00000000..9b1d4b78 --- /dev/null +++ b/test/stableBTreeTest/module/testBTreeMap.mo @@ -0,0 +1,1255 @@ +import Types "../../src/types"; +import Memory "../../src/memory"; +import BTreeMap "../../src/btreemap"; +import Node "../../src/node"; +import Utils "../../src/utils"; +import Conversion "../../src/conversion"; +import TestableItems "testableItems"; + +import Nat64 "mo:base/Nat64"; +import Nat32 "mo:base/Nat32"; +import Nat8 "mo:base/Nat8"; +import Iter "mo:base/Iter"; +import Int "mo:base/Int"; + +module { + + // For convenience: from base module + type Iter = Iter.Iter; + // For convenience: from types module + type Entry = Types.Entry; + // For convenience: from other modules + type BTreeMap = BTreeMap.BTreeMap; + type TestBuffer = TestableItems.TestBuffer; + + // A helper method to succinctly create an entry. + func e(x: Nat8) : Entry { + ([x], []); + }; + + let bytes_passtrough = { + fromBytes = func(bytes: [Nat8]) : [Nat8] { bytes; }; + toBytes = func(bytes: [Nat8]) : [Nat8] { bytes; }; + }; + + func initPreservesData(test: TestBuffer) { + let mem = Memory.VecMemory(); + var btree = BTreeMap.init<[Nat8], [Nat8]>(mem, 3, 4, bytes_passtrough, bytes_passtrough); + test.equalsInsertResult(btree.insert([1, 2, 3], [4, 5, 6]), #ok(null)); + test.equalsOptBytes(btree.get([1, 2, 3]), ?([4, 5, 6])); + + // Reload the btree + btree := BTreeMap.init<[Nat8], [Nat8]>(mem, 3, 4, bytes_passtrough, bytes_passtrough); + + // Data still exists. + test.equalsOptBytes(btree.get([1, 2, 3]), ?([4, 5, 6])); + }; + + func insertGet(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 3, 4, bytes_passtrough, bytes_passtrough); + + test.equalsInsertResult(btree.insert([1, 2, 3], [4, 5, 6]), #ok(null)); + test.equalsOptBytes(btree.get([1, 2, 3]), ?([4, 5, 6])); + }; + + func insertOverwritesPreviousValue(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + test.equalsInsertResult(btree.insert([1, 2, 3], [4, 5, 6]), #ok(null)); + test.equalsInsertResult(btree.insert([1, 2, 3], [7, 8, 9]), #ok(?([4, 5, 6]))); + test.equalsOptBytes(btree.get([1, 2, 3]), ?([7, 8, 9])); + }; + + func insertGetMultiple(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + test.equalsInsertResult(btree.insert([1, 2, 3] , [4, 5, 6]), #ok(null)); + test.equalsInsertResult(btree.insert([4, 5] , [7, 8, 9, 10]), #ok(null)); + test.equalsInsertResult(btree.insert([], [11]), #ok(null)); + test.equalsOptBytes(btree.get([1, 2, 3]), ?([4, 5, 6])); + test.equalsOptBytes(btree.get([4, 5]), ?([7, 8, 9, 10])); + test.equalsOptBytes(btree.get([]), ?([11])); + }; + + func insertOverwriteMedianKeyInFullChildNode(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 17)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + + // The result should look like this: + // [6] + // / \ + // [1, 2, 3, 4, 5] [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] + + let root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [Node.makeEntry([6], [])]); + test.equalsNat(root.getChildren().size(), 2); + + // The right child should now be full, with the median key being "12" + var right_child = btree.loadNode(root.getChildren().get(1)); + test.equalsBool(right_child.isFull(), true); + let median_index = right_child.getEntries().size() / 2; + test.equalsBytes(right_child.getEntries().get(median_index).0, [12]); + + // Overwrite the median key. + test.equalsInsertResult(btree.insert([12], [1, 2, 3]), #ok(?([]))); + + // The key is overwritten successfully. + test.equalsOptBytes(btree.get([12]), ?([1, 2, 3])); + + // The child has not been split and is still full. + right_child := btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(right_child.getNodeType(), #Leaf); + test.equalsBool(right_child.isFull(), true); + }; + + func insertOverwriteKeyInFullRootNode(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 11)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + + // We now have a root that is full and looks like this: + // + // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + var root = btree.loadNode(btree.getRootAddr()); + test.equalsBool(root.isFull(), true); + + // Overwrite an element in the root. It should NOT cause the node to be split. + test.equalsInsertResult(btree.insert([6], [4, 5, 6]), #ok(?([]))); + + root := btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Leaf); + test.equalsOptBytes(btree.get([6]), ?([4, 5, 6])); + test.equalsNat(root.getEntries().size(), 11); + }; + + func allocations(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(0, Nat64.toNat(Node.getCapacity() - 1))) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + + // Only need a single allocation to store up to `CAPACITY` elements. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 1); + + test.equalsInsertResult(btree.insert([255], []), #ok(null)); + + // The node had to be split into three nodes. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 3); + }; + + func allocations2(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 0); + + test.equalsInsertResult(btree.insert([], []), #ok(null)); + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 1); + + test.equalsOptBytes(btree.remove([]), ?([])); + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 0); + }; + + func insertSameKeyMultiple(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + test.equalsInsertResult(btree.insert([1], [2]), #ok(null)); + + for (i in Iter.range(2, 9)) { + test.equalsInsertResult(btree.insert([1], [Nat8.fromNat(i) + 1]), #ok(?([Nat8.fromNat(i)]))); + }; + }; + + func insertSplitNode(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 11)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + + // Should now split a node. + test.equalsInsertResult(btree.insert([12], []), #ok(null)); + + // The result should look like this: + // [6] + // / \ + // [1, 2, 3, 4, 5] [7, 8, 9, 10, 11, 12] + + for (i in Iter.range(1, 12)) { + test.equalsOptBytes(btree.get([Nat8.fromNat(i)]), ?([])); + }; + }; + + func overwriteTest(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + let num_elements = 255; + + // Ensure that the number of elements we insert is significantly + // higher than `CAPACITY` so that we test interesting cases (e.g. + // overwriting the value in an internal node). + assert(Nat64.fromNat(num_elements) > 10 * Node.getCapacity()); + + for (i in Iter.range(0, num_elements - 1)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + + // Overwrite the values. + for (i in Iter.range(0, num_elements - 1)) { + // Assert we retrieved the old value correctly. + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], [1, 2, 3]), #ok(?([]))); + // Assert we retrieved the new value correctly. + test.equalsOptBytes(btree.get([Nat8.fromNat(i)]), ?([1, 2, 3])); + }; + }; + + func insertSplitMultipleNodes(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 11)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + // Should now split a node. + test.equalsInsertResult(btree.insert([12], []), #ok(null)); + + // The result should look like this: + // [6] + // / \ + // [1, 2, 3, 4, 5] [7, 8, 9, 10, 11, 12] + + var root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [([6], [])]); + test.equalsNat(root.getChildren().size(), 2); + + var child_0 = btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries( + child_0.getEntries().toArray(), + [ + ([1], []), + ([2], []), + ([3], []), + ([4], []), + ([5], []) + ] + ); + + var child_1 = btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries( + child_1.getEntries().toArray(), + [ + ([7], []), + ([8], []), + ([9], []), + ([10], []), + ([11], []), + ([12], []) + ] + ); + + for (i in Iter.range(1, 12)) { + test.equalsOptBytes(btree.get([Nat8.fromNat(i)]), ?([])); + }; + + // Insert more to cause more splitting. + test.equalsInsertResult(btree.insert([13], []), #ok(null)); + test.equalsInsertResult(btree.insert([14], []), #ok(null)); + test.equalsInsertResult(btree.insert([15], []), #ok(null)); + test.equalsInsertResult(btree.insert([16], []), #ok(null)); + test.equalsInsertResult(btree.insert([17], []), #ok(null)); + // Should cause another split + test.equalsInsertResult(btree.insert([18], []), #ok(null)); + + for (i in Iter.range(1, 18)) { + test.equalsOptBytes(btree.get([Nat8.fromNat(i)]), ?([])); + }; + + root := btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [([6], []), ([12], [])]); + test.equalsNat(root.getChildren().size(), 3); + + child_0 := btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries( + child_0.getEntries().toArray(), + [ + ([1], []), + ([2], []), + ([3], []), + ([4], []), + ([5], []) + ] + ); + + child_1 := btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries( + child_1.getEntries().toArray(), + [ + ([7], []), + ([8], []), + ([9], []), + ([10], []), + ([11], []), + ] + ); + + let child_2 = btree.loadNode(root.getChildren().get(2)); + test.equalsNodeType(child_2.getNodeType(), #Leaf); + test.equalsEntries( + child_2.getEntries().toArray(), + [ + ([13], []), + ([14], []), + ([15], []), + ([16], []), + ([17], []), + ([18], []), + ] + ); + }; + + func removeSimple(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + test.equalsInsertResult(btree.insert([1, 2, 3], [4, 5, 6]), #ok(null)); + test.equalsOptBytes(btree.get([1, 2, 3]), ?([4, 5, 6])); + test.equalsOptBytes(btree.remove([1, 2, 3]), ?([4, 5, 6])); + test.equalsOptBytes(btree.get([1, 2, 3]), null); + }; + + func removeCase2aAnd2c(test: TestBuffer) { + let mem = Memory.VecMemory(); + var btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 11)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + // Should now split a node. + test.equalsInsertResult(btree.insert([0], []), #ok(null)); + + // The result should look like this: + // [6] + // / \ + // [0, 1, 2, 3, 4, 5] [7, 8, 9, 10, 11] + + for (i in Iter.range(0, 11)) { + test.equalsOptBytes(btree.get([Nat8.fromNat(i)]), ?([])); + }; + + // Remove node 6. Triggers case 2.a + test.equalsOptBytes(btree.remove([6]), ?([])); + + // The result should look like this: + // [5] + // / \ + // [0, 1, 2, 3, 4] [7, 8, 9, 10, 11] + var root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [e(5)]); + test.equalsNat(root.getChildren().size(), 2); + + let child_0 = btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries(child_0.getEntries().toArray(), [e(0), e(1), e(2), e(3), e(4)]); + + let child_1 = btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries(child_1.getEntries().toArray(), [e(7), e(8), e(9), e(10), e(11)]); + + // There are three allocated nodes. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 3); + + // Remove node 5. Triggers case 2c + test.equalsOptBytes(btree.remove([5]), ?([])); + + // Reload the btree to verify that we saved it correctly. + btree := BTreeMap.load(mem, bytes_passtrough, bytes_passtrough); + + // The result should look like this: + // [0, 1, 2, 3, 4, 7, 8, 9, 10, 11] + root := btree.loadNode(btree.getRootAddr()); + test.equalsEntries( + root.getEntries().toArray(), + [e(0), e(1), e(2), e(3), e(4), e(7), e(8), e(9), e(10), e(11)] + ); + + // There is only one node allocated. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 1); + }; + + func removeCase2b(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 11)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + // Should now split a node. + test.equalsInsertResult(btree.insert([12], []), #ok(null)); + + // The result should look like this: + // [6] + // / \ + // [1, 2, 3, 4, 5] [7, 8, 9, 10, 11, 12] + + for (i in Iter.range(1, 12)) { + test.equalsOptBytes(btree.get([Nat8.fromNat(i)]), ?([])); + }; + + // Remove node 6. Triggers case 2.b + test.equalsOptBytes(btree.remove([6]), ?([])); + + // The result should look like this: + // [7] + // / \ + // [1, 2, 3, 4, 5] [8, 9, 10, 11, 12] + var root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [e(7)]); + test.equalsNat(root.getChildren().size(), 2); + + let child_0 = btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries(child_0.getEntries().toArray(), [e(1), e(2), e(3), e(4), e(5)]); + + let child_1 = btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries(child_1.getEntries().toArray(), [e(8), e(9), e(10), e(11), e(12)]); + + // Remove node 7. Triggers case 2.c + test.equalsOptBytes(btree.remove([7]), ?([])); + // The result should look like this: + // + // [1, 2, 3, 4, 5, 8, 9, 10, 11, 12] + root := btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Leaf); + test.equalsEntries( + root.getEntries().toArray(), + [ + e(1), + e(2), + e(3), + e(4), + e(5), + e(8), + e(9), + e(10), + e(11), + e(12) + ] + ); + }; + + func removeCase3aRight(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 11)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + + // Should now split a node. + test.equalsInsertResult(btree.insert([12], []), #ok(null)); + + // The result should look like this: + // [6] + // / \ + // [1, 2, 3, 4, 5] [7, 8, 9, 10, 11, 12] + + // Remove node 3. Triggers case 3.a + test.equalsOptBytes(btree.remove([3]), ?([])); + + // The result should look like this: + // [7] + // / \ + // [1, 2, 4, 5, 6] [8, 9, 10, 11, 12] + let root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [([7], [])]); + test.equalsNat(root.getChildren().size(), 2); + + let child_0 = btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries(child_0.getEntries().toArray(), [e(1), e(2), e(4), e(5), e(6)]); + + let child_1 = btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries(child_1.getEntries().toArray(), [e(8), e(9), e(10), e(11), e(12)]); + + // There are three allocated nodes. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 3); + }; + + func removeCase3aLeft(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 11)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + // Should now split a node. + test.equalsInsertResult(btree.insert([0], []), #ok(null)); + + // The result should look like this: + // [6] + // / \ + // [0, 1, 2, 3, 4, 5] [7, 8, 9, 10, 11] + + // Remove node 8. Triggers case 3.a left + test.equalsOptBytes(btree.remove([8]), ?([])); + + // The result should look like this: + // [5] + // / \ + // [0, 1, 2, 3, 4] [6, 7, 9, 10, 11] + let root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [([5], [])]); + test.equalsNat(root.getChildren().size(), 2); + + let child_0 = btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries(child_0.getEntries().toArray(), [e(0), e(1), e(2), e(3), e(4)]); + + let child_1 = btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries(child_1.getEntries().toArray(), [e(6), e(7), e(9), e(10), e(11)]); + + // There are three allocated nodes. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 3); + }; + + func removeCase3bMergeIntoRight(test: TestBuffer) { + let mem = Memory.VecMemory(); + var btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 11)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + // Should now split a node. + test.equalsInsertResult(btree.insert([12], []), #ok(null)); + + // The result should look like this: + // [6] + // / \ + // [1, 2, 3, 4, 5] [7, 8, 9, 10, 11, 12] + + for (i in Iter.range(1, 12)) { + test.equalsOptBytes(btree.get([Nat8.fromNat(i)]), ?([])); + }; + + // Remove node 6. Triggers case 2.b + test.equalsOptBytes(btree.remove([6]), ?([])); + // The result should look like this: + // [7] + // / \ + // [1, 2, 3, 4, 5] [8, 9, 10, 11, 12] + var root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [([7], [])]); + test.equalsNat(root.getChildren().size(), 2); + + let child_0 = btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries(child_0.getEntries().toArray(), [e(1), e(2), e(3), e(4), e(5)]); + + let child_1 = btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries(child_1.getEntries().toArray(), [e(8), e(9), e(10), e(11), e(12)]); + + // There are three allocated nodes. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 3); + + // Remove node 3. Triggers case 3.b + test.equalsOptBytes(btree.remove([3]), ?([])); + + // Reload the btree to verify that we saved it correctly. + btree := BTreeMap.load(mem, bytes_passtrough, bytes_passtrough); + + // The result should look like this: + // + // [1, 2, 4, 5, 7, 8, 9, 10, 11, 12] + root := btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Leaf); + test.equalsEntries( + root.getEntries().toArray(), + [ + e(1), + e(2), + e(4), + e(5), + e(7), + e(8), + e(9), + e(10), + e(11), + e(12) + ] + ); + + // There is only one allocated node remaining. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 1); + }; + + func removeCase3bMergeIntoLeft(test: TestBuffer) { + let mem = Memory.VecMemory(); + var btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 11)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + + // Should now split a node. + test.equalsInsertResult(btree.insert([12], []), #ok(null)); + + // The result should look like this: + // [6] + // / \ + // [1, 2, 3, 4, 5] [7, 8, 9, 10, 11, 12] + + for (i in Iter.range(1, 12)) { + test.equalsOptBytes(btree.get([Nat8.fromNat(i)]), ?([])); + }; + + // Remove node 6. Triggers case 2.b + test.equalsOptBytes(btree.remove([6]), ?([])); + + // The result should look like this: + // [7] + // / \ + // [1, 2, 3, 4, 5] [8, 9, 10, 11, 12] + var root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [([7], [])]); + test.equalsNat(root.getChildren().size(), 2); + + let child_0 = btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries(child_0.getEntries().toArray(), [e(1), e(2), e(3), e(4), e(5)]); + + let child_1 = btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries(child_1.getEntries().toArray(), [e(8), e(9), e(10), e(11), e(12)]); + + // There are three allocated nodes. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 3); + + // Remove node 10. Triggers case 3.b where we merge the right into the left. + test.equalsOptBytes(btree.remove([10]), ?([])); + + // Reload the btree to verify that we saved it correctly. + btree := BTreeMap.load(mem, bytes_passtrough, bytes_passtrough); + + // The result should look like this: + // + // [1, 2, 3, 4, 5, 7, 8, 9, 11, 12] + root := btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Leaf); + test.equalsEntries( + root.getEntries().toArray(), + [e(1), e(2), e(3), e(4), e(5), e(7), e(8), e(9), e(11), e(12)] + ); + + // There is only one allocated node remaining. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 1); + }; + + func manyInsertions(test: TestBuffer) { + let mem = Memory.VecMemory(); + var btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (j in Iter.range(0, 10)) { + for (i in Iter.range(0, 255)) { + let bytes = [Nat8.fromNat(i), Nat8.fromNat(j)]; + test.equalsInsertResult(btree.insert(bytes, bytes), #ok(null)); + }; + }; + + for (j in Iter.range(0, 10)) { + for (i in Iter.range(0, 255)) { + let bytes = [Nat8.fromNat(i), Nat8.fromNat(j)]; + test.equalsOptBytes(btree.get(bytes), ?(bytes)); + }; + }; + + btree := BTreeMap.load(mem, bytes_passtrough, bytes_passtrough); + + for (j in Iter.range(0, 10)) { + for (i in Iter.range(0, 255)) { + let bytes = [Nat8.fromNat(i), Nat8.fromNat(j)]; + test.equalsOptBytes(btree.remove(bytes), ?(bytes)); + }; + }; + + for (j in Iter.range(0, 10)) { + for (i in Iter.range(0, 255)) { + let bytes = [Nat8.fromNat(i), Nat8.fromNat(j)]; + test.equalsOptBytes(btree.get(bytes), null); + }; + }; + + // We've deallocated everything. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 0); + }; + + func manyInsertions2(test: TestBuffer) { + let mem = Memory.VecMemory(); + var btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (j in Iter.revRange(10, 0)) { + for (i in Iter.revRange(255, 0)) { + let bytes = [Nat8.fromNat(Int.abs(i)), Nat8.fromNat(Int.abs(j))]; + test.equalsInsertResult(btree.insert(bytes, bytes), #ok(null)); + }; + }; + + for (j in Iter.range(0, 10)) { + for (i in Iter.range(0, 255)) { + let bytes = [Nat8.fromNat(i), Nat8.fromNat(j)]; + test.equalsOptBytes(btree.get(bytes), ?(bytes)); + }; + }; + + btree := BTreeMap.load(mem, bytes_passtrough, bytes_passtrough); + + for (j in Iter.revRange(10, 0)) { + for (i in Iter.revRange((255, 0))) { + let bytes = [Nat8.fromNat(Int.abs(i)), Nat8.fromNat(Int.abs(j))]; + test.equalsOptBytes(btree.remove(bytes), ?(bytes)); + }; + }; + + for (j in Iter.range(0, 10)) { + for (i in Iter.range(0, 255)) { + let bytes = [Nat8.fromNat(i), Nat8.fromNat(j)]; + test.equalsOptBytes(btree.get(bytes), null); + }; + }; + + // We've deallocated everything. + test.equalsNat64(btree.getAllocator().getNumAllocatedChunks(), 0); + }; + + func reloading(test: TestBuffer) { + let mem = Memory.VecMemory(); + var btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + // The btree is initially empty. + test.equalsNat64(btree.getLength(), 0); + test.equalsBool(btree.isEmpty(), true); + + // Add an entry into the btree. + test.equalsInsertResult(btree.insert([1, 2, 3], [4, 5, 6]) , #ok(null)); + test.equalsNat64(btree.getLength(), 1); + test.equalsBool(btree.isEmpty(), false); + + // Reload the btree. The element should still be there, and `len()` + // should still be `1`. + btree := BTreeMap.load(mem, bytes_passtrough, bytes_passtrough); + test.equalsOptBytes(btree.get([1, 2, 3]), ?([4, 5, 6])); + test.equalsNat64(btree.getLength(), 1); + test.equalsBool(btree.isEmpty(), false); + + // Remove an element. Length should be zero. + btree := BTreeMap.load(mem, bytes_passtrough, bytes_passtrough); + test.equalsOptBytes(btree.remove([1, 2, 3]), ?([4, 5, 6])); + test.equalsNat64(btree.getLength(), 0); + test.equalsBool(btree.isEmpty(), true); + + // Reload. Btree should still be empty. + btree := BTreeMap.load(mem, bytes_passtrough, bytes_passtrough); + test.equalsOptBytes(btree.get([1, 2, 3]), null); + test.equalsNat64(btree.getLength(), 0); + test.equalsBool(btree.isEmpty(), true); + }; + + func len(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(0, 999)) { + test.equalsInsertResult(btree.insert(Conversion.nat32ToBytes(Nat32.fromNat(i)), []) , #ok(null)); + }; + + test.equalsNat64(btree.getLength(), 1000); + test.equalsBool(btree.isEmpty(), false); + + for (i in Iter.range(0, 999)) { + test.equalsOptBytes(btree.remove(Conversion.nat32ToBytes(Nat32.fromNat(i))), ?([])); + }; + + test.equalsNat64(btree.getLength(), 0); + test.equalsBool(btree.isEmpty(), true); + }; + + func containsKey(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + // Insert even numbers from 0 to 1000. + for (i in Iter.range(0, 499)) { + test.equalsInsertResult(btree.insert(Conversion.nat32ToBytes(Nat32.fromNat(i * 2)), []), #ok(null)); + }; + + // Contains key should return true on all the even numbers and false on all the odd + // numbers. + for (i in Iter.range(0, 499)) { + test.equalsBool(btree.containsKey(Conversion.nat32ToBytes(Nat32.fromNat(i))), (i % 2 == 0)); + }; + }; + + func rangeEmpty(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + // Test prefixes that don't exist in the map. + test.equalsEntries(Iter.toArray(btree.range([0], null)), []); + test.equalsEntries(Iter.toArray(btree.range([1, 2, 3, 4], null)), []); + }; + + // Tests the case where the prefix is larger than all the entries in a leaf node. + func rangeLeafPrefixGreaterThanAllEntries(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + ignore btree.insert([0], []); + + // Test a prefix that's larger than the value in the leaf node. Should be empty. + test.equalsEntries(Iter.toArray(btree.range([1], null)), []); + }; + + // Tests the case where the prefix is larger than all the entries in an internal node. + func rangeInternalPrefixGreaterThanAllEntries(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(1, 12)) { + test.equalsInsertResult(btree.insert([Nat8.fromNat(i)], []), #ok(null)); + }; + + // The result should look like this: + // [6] + // / \ + // [1, 2, 3, 4, 5] [7, 8, 9, 10, 11, 12] + + // Test a prefix that's larger than the value in the internal node. + test.equalsEntries( + Iter.toArray(btree.range([7], null)), + [([7], [])] + ); + }; + + func rangeVariousPrefixes(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + ignore btree.insert([0, 1], []); + ignore btree.insert([0, 2], []); + ignore btree.insert([0, 3], []); + ignore btree.insert([0, 4], []); + ignore btree.insert([1, 1], []); + ignore btree.insert([1, 2], []); + ignore btree.insert([1, 3], []); + ignore btree.insert([1, 4], []); + ignore btree.insert([2, 1], []); + ignore btree.insert([2, 2], []); + ignore btree.insert([2, 3], []); + ignore btree.insert([2, 4], []); + + // The result should look like this: + // [(1, 2)] + // / \ + // [(0, 1), (0, 2), (0, 3), (0, 4), (1, 1)] [(1, 3), (1, 4), (2, 1), (2, 2), (2, 3), (2, 4)] + + let root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [([1, 2], [])]); + test.equalsNat(root.getChildren().size(), 2); + + // Tests a prefix that's smaller than the value in the internal node. + test.equalsEntries( + Iter.toArray(btree.range([0], null)), + [ + ([0, 1], []), + ([0, 2], []), + ([0, 3], []), + ([0, 4], []), + ] + ); + + // Tests a prefix that crosses several nodes. + test.equalsEntries( + Iter.toArray(btree.range([1], null)), + [ + ([1, 1], []), + ([1, 2], []), + ([1, 3], []), + ([1, 4], []), + ] + ); + + // Tests a prefix that's larger than the value in the internal node. + test.equalsEntries( + Iter.toArray(btree.range([2], null)), + [ + ([2, 1], []), + ([2, 2], []), + ([2, 3], []), + ([2, 4], []), + ] + ); + }; + + func rangeVariousPrefixes2(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + ignore btree.insert([0, 1], []); + ignore btree.insert([0, 2], []); + ignore btree.insert([0, 3], []); + ignore btree.insert([0, 4], []); + ignore btree.insert([1, 2], []); + ignore btree.insert([1, 4], []); + ignore btree.insert([1, 6], []); + ignore btree.insert([1, 8], []); + ignore btree.insert([1, 10], []); + ignore btree.insert([2, 1], []); + ignore btree.insert([2, 2], []); + ignore btree.insert([2, 3], []); + ignore btree.insert([2, 4], []); + ignore btree.insert([2, 5], []); + ignore btree.insert([2, 6], []); + ignore btree.insert([2, 7], []); + ignore btree.insert([2, 8], []); + ignore btree.insert([2, 9], []); + + // The result should look like this: + // [(1, 4), (2, 3)] + // / | \ + // [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2)] | [(2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9)] + // | + // [(1, 6), (1, 8), (1, 10), (2, 1), (2, 2)] + let root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries( + root.getEntries().toArray(), + [([1, 4], []), ([2, 3], [])] + ); + test.equalsNat(root.getChildren().size(), 3); + + let child_0 = btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries( + child_0.getEntries().toArray(), + [ + ([0, 1], []), + ([0, 2], []), + ([0, 3], []), + ([0, 4], []), + ([1, 2], []), + ] + ); + + let child_1 = btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries( + child_1.getEntries().toArray(), + [ + ([1, 6], []), + ([1, 8], []), + ([1, 10], []), + ([2, 1], []), + ([2, 2], []), + ] + ); + + let child_2 = btree.loadNode(root.getChildren().get(2)); + test.equalsEntries( + child_2.getEntries().toArray(), + [ + ([2, 4], []), + ([2, 5], []), + ([2, 6], []), + ([2, 7], []), + ([2, 8], []), + ([2, 9], []), + ] + ); + + // Tests a prefix that doesn't exist, but is in the middle of the root node. + test.equalsEntries(Iter.toArray(btree.range([1, 5], null)), []); + + // Tests a prefix that crosses several nodes. + test.equalsEntries( + Iter.toArray(btree.range([1], null)), + [ + ([1, 2], []), + ([1, 4], []), + ([1, 6], []), + ([1, 8], []), + ([1, 10], []), + ] + ); + + // Tests a prefix that starts from a leaf node, then iterates through the root and right + // sibling. + test.equalsEntries( + Iter.toArray(btree.range([2], null)), + [ + ([2, 1], []), + ([2, 2], []), + ([2, 3], []), + ([2, 4], []), + ([2, 5], []), + ([2, 6], []), + ([2, 7], []), + ([2, 8], []), + ([2, 9], []), + ] + ); + }; + + func rangeLarge(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + // Insert 1000 elements with prefix 0 and another 1000 elements with prefix 1. + for (prefix in Iter.range(0, 1)) { + for (i in Iter.range(0, 999)) { + // The key is the prefix followed by the integer's encoding. + // The encoding is big-endian so that the byte representation of the + // integers are sorted. + // @todo: here it is supposed to be in big endian! + let key = Utils.append([Nat8.fromNat(prefix)], Conversion.nat32ToBytes(Nat32.fromNat(i))); + test.equalsInsertResult(btree.insert(key, []), #ok(null)); + }; + }; + + // Getting the range with a prefix should return all 1000 elements with that prefix. + for (prefix in Iter.range(0, 1)) { + var i : Nat32 = 0; + for ((key, _) in btree.range([Nat8.fromNat(prefix)], null)) { + // @todo: here it is supposed to be in big endian! + test.equalsBytes(key, Utils.append([Nat8.fromNat(prefix)], Conversion.nat32ToBytes(i))); + i += 1; + }; + test.equalsNat32(i, 1000); + }; + }; + + func rangeVariousPrefixesWithOffset(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + ignore btree.insert([0, 1], []); + ignore btree.insert([0, 2], []); + ignore btree.insert([0, 3], []); + ignore btree.insert([0, 4], []); + ignore btree.insert([1, 1], []); + ignore btree.insert([1, 2], []); + ignore btree.insert([1, 3], []); + ignore btree.insert([1, 4], []); + ignore btree.insert([2, 1], []); + ignore btree.insert([2, 2], []); + ignore btree.insert([2, 3], []); + ignore btree.insert([2, 4], []); + + // The result should look like this: + // [(1, 2)] + // / \ + // [(0, 1), (0, 2), (0, 3), (0, 4), (1, 1)] [(1, 3), (1, 4), (2, 1), (2, 2), (2, 3), (2, 4)] + + let root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries(root.getEntries().toArray(), [([1, 2], [])]); + test.equalsNat(root.getChildren().size(), 2); + + // Tests a offset that's smaller than the value in the internal node. + test.equalsEntries( + Iter.toArray(btree.range([0], ?([0]))), + [ + ([0, 1], []), + ([0, 2], []), + ([0, 3], []), + ([0, 4], []), + ] + ); + + // Tests a offset that has a value somewhere in the range of values of an internal node. + test.equalsEntries( + Iter.toArray(btree.range([1], ?([3]))), + [([1, 3], []), ([1, 4], []),] + ); + + // Tests a offset that's larger than the value in the internal node. + test.equalsEntries( + Iter.toArray(btree.range([2], ?([5]))), + [], + ); + }; + + func rangeVariousPrefixesWithOffset2(test: TestBuffer) { + let mem = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(mem, 5, 5, bytes_passtrough, bytes_passtrough); + + ignore btree.insert([0, 1], []); + ignore btree.insert([0, 2], []); + ignore btree.insert([0, 3], []); + ignore btree.insert([0, 4], []); + ignore btree.insert([1, 2], []); + ignore btree.insert([1, 4], []); + ignore btree.insert([1, 6], []); + ignore btree.insert([1, 8], []); + ignore btree.insert([1, 10], []); + ignore btree.insert([2, 1], []); + ignore btree.insert([2, 2], []); + ignore btree.insert([2, 3], []); + ignore btree.insert([2, 4], []); + ignore btree.insert([2, 5], []); + ignore btree.insert([2, 6], []); + ignore btree.insert([2, 7], []); + ignore btree.insert([2, 8], []); + ignore btree.insert([2, 9], []); + + // The result should look like this: + // [(1, 4), (2, 3)] + // / | \ + // [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2)] | [(2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9)] + // | + // [(1, 6), (1, 8), (1, 10), (2, 1), (2, 2)] + let root = btree.loadNode(btree.getRootAddr()); + test.equalsNodeType(root.getNodeType(), #Internal); + test.equalsEntries( + root.getEntries().toArray(), + [([1, 4], []), ([2, 3], [])] + ); + test.equalsNat(root.getChildren().size(), 3); + + let child_0 = btree.loadNode(root.getChildren().get(0)); + test.equalsNodeType(child_0.getNodeType(), #Leaf); + test.equalsEntries( + child_0.getEntries().toArray(), + [ + ([0, 1], []), + ([0, 2], []), + ([0, 3], []), + ([0, 4], []), + ([1, 2], []), + ] + ); + + let child_1 = btree.loadNode(root.getChildren().get(1)); + test.equalsNodeType(child_1.getNodeType(), #Leaf); + test.equalsEntries( + child_1.getEntries().toArray(), + [ + ([1, 6], []), + ([1, 8], []), + ([1, 10], []), + ([2, 1], []), + ([2, 2], []), + ] + ); + + let child_2 = btree.loadNode(root.getChildren().get(2)); + test.equalsEntries( + child_2.getEntries().toArray(), + [ + ([2, 4], []), + ([2, 5], []), + ([2, 6], []), + ([2, 7], []), + ([2, 8], []), + ([2, 9], []), + ] + ); + + // Tests a offset that crosses several nodes. + test.equalsEntries( + Iter.toArray(btree.range([1], ?([4]))), + [ + ([1, 4], []), + ([1, 6], []), + ([1, 8], []), + ([1, 10], []), + ] + ); + + // Tests a offset that starts from a leaf node, then iterates through the root and right + // sibling. + test.equalsEntries( + Iter.toArray(btree.range([2], ?([2]))), + [ + ([2, 2], []), + ([2, 3], []), + ([2, 4], []), + ([2, 5], []), + ([2, 6], []), + ([2, 7], []), + ([2, 8], []), + ([2, 9], []), + ] + ); + }; + + public func run() { + let test = TestableItems.TestBuffer(); + + initPreservesData(test); + insertGet(test); + insertOverwritesPreviousValue(test); + insertGetMultiple(test); + insertOverwriteMedianKeyInFullChildNode(test); + insertOverwriteKeyInFullRootNode(test); + allocations(test); + allocations2(test); + insertSameKeyMultiple(test); + insertSplitNode(test); + overwriteTest(test); + insertSplitMultipleNodes(test); + removeSimple(test); + removeCase2aAnd2c(test); + removeCase2b(test); + removeCase3aRight(test); + removeCase3aLeft(test); + removeCase3bMergeIntoRight(test); + removeCase3bMergeIntoLeft(test); + manyInsertions(test); + manyInsertions2(test); + reloading(test); + len(test); + containsKey(test); + rangeEmpty(test); + rangeLeafPrefixGreaterThanAllEntries(test); + rangeInternalPrefixGreaterThanAllEntries(test); + rangeVariousPrefixes(test); + rangeVariousPrefixes2(test); + rangeLarge(test); + rangeVariousPrefixesWithOffset(test); + rangeVariousPrefixesWithOffset2(test); + + test.run("Test btreemap module"); + }; + +}; \ No newline at end of file diff --git a/test/stableBTreeTest/module/testIter.mo b/test/stableBTreeTest/module/testIter.mo new file mode 100644 index 00000000..9b16e8c1 --- /dev/null +++ b/test/stableBTreeTest/module/testIter.mo @@ -0,0 +1,75 @@ +import Memory "../../src/memory"; +import BTreeMap "../../src/btreemap"; +import Node "../../src/node"; +import TestableItems "testableItems"; + +import Iter "mo:base/Iter"; +import Nat64 "mo:base/Nat64"; +import Nat8 "mo:base/Nat8"; +import Int "mo:base/Int"; + +module { + + // For convenience: from other modules + type TestBuffer = TestableItems.TestBuffer; + + func iterateLeaf(test: TestBuffer) { + let bytes_passtrough = { + fromBytes = func(bytes: [Nat8]) : [Nat8] { bytes; }; + toBytes = func(bytes: [Nat8]) : [Nat8] { bytes; }; + }; + + // Iterate on leaf + let memory = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(memory, 1, 1, bytes_passtrough, bytes_passtrough); + + for (i in Iter.range(0, Nat64.toNat(Node.getCapacity() - 1))){ + ignore btree.insert([Nat8.fromNat(i)], [Nat8.fromNat(i + 1)]); + }; + + var i : Nat8 = 0; + for ((key, value) in btree.iter()){ + test.equalsBytes(key, [i]); + test.equalsBytes(value, [i + 1]); + i += 1; + }; + + test.equalsNat(Nat8.toNat(i), Nat64.toNat(Node.getCapacity())); + }; + + func iterateChildren(test: TestBuffer) { + let bytes_passtrough = { + fromBytes = func(bytes: [Nat8]) : [Nat8] { bytes; }; + toBytes = func(bytes: [Nat8]) : [Nat8] { bytes; }; + }; + + // Iterate on leaf + let memory = Memory.VecMemory(); + let btree = BTreeMap.new<[Nat8], [Nat8]>(memory, 1, 1, bytes_passtrough, bytes_passtrough); + + // Insert the elements in reverse order. + for (i in Iter.revRange(99, 0)){ + ignore btree.insert([Nat8.fromNat(Int.abs(i))], [Nat8.fromNat(Int.abs(i + 1))]); + }; + + // Iteration should be in ascending order. + var i : Nat8 = 0; + for ((key, value) in btree.iter()){ + test.equalsBytes(key, [i]); + test.equalsBytes(value, [i + 1]); + i += 1; + }; + + test.equalsNat8(i, 100); + }; + + public func run() { + let test = TestableItems.TestBuffer(); + + iterateLeaf(test); + iterateChildren(test); + + test.run("Test iter module"); + }; + +}; \ No newline at end of file diff --git a/test/stableBTreeTest/module/testMemoryManager.mo b/test/stableBTreeTest/module/testMemoryManager.mo new file mode 100644 index 00000000..1c11ab33 --- /dev/null +++ b/test/stableBTreeTest/module/testMemoryManager.mo @@ -0,0 +1,272 @@ +import MemoryManager "../../src/memoryManager"; +import Memory "../../src/memory"; +import Constants "../../src/constants"; +import TestableItems "testableItems"; + +import Nat64 "mo:base/Nat64"; +import Nat16 "mo:base/Nat16"; +import Int64 "mo:base/Int64"; +import Buffer "mo:base/Buffer"; +import Array "mo:base/Array"; + +module { + + // For convenience: from the memory manager module + type MemoryId = MemoryManager.MemoryId; + type BucketId = MemoryManager.BucketId; + // For convenience: from base module + type Buffer = Buffer.Buffer; + // For convenience: from other modules + type TestBuffer = TestableItems.TestBuffer; + + public func toOptArray(buffer: ?Buffer) : ?[T] { + switch(buffer){ + case(null) { null; }; + case(?buffer) { ?buffer.toArray(); }; + }; + }; + + // To use less memory and avoid RTS error: Cannot grow memory + let BUCKET_SIZE_IN_PAGES : Nat64 = 16; + + func canGetMemory(test: TestBuffer) { + let mem_mgr = MemoryManager.initWithBuckets(Memory.VecMemory(), Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory = mem_mgr.get(0 : MemoryId); + test.equalsNat64(memory.size(), 0); + }; + + func canAllocateAndUseMemory(test: TestBuffer) { + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory = mem_mgr.get(0 : MemoryId); + + test.equalsInt64(memory.grow(1), 0); + test.equalsNat64(memory.size(), 1); + + memory.write(0, [1, 2, 3]); + + let bytes = memory.read(0, 3); + test.equalsBytes(bytes, [1, 2, 3]); + + test.equalsOptArrayNat16(toOptArray(mem_mgr.inner_.memory_buckets_.get(0 : MemoryId)), ?[0 : BucketId]); + }; + + func canAllocateAndUseMultipleMemories(test: TestBuffer) { + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + let memory_1 = mem_mgr.get(1 : MemoryId); + + test.equalsInt64(memory_0.grow(1), 0); + test.equalsInt64(memory_1.grow(1), 0); + + test.equalsNat64(memory_0.size(), 1); + test.equalsNat64(memory_1.size(), 1); + + test.equalsOptArrayNat16(toOptArray(mem_mgr.inner_.memory_buckets_.get(0 : MemoryId)), ?[0 : BucketId]); + test.equalsOptArrayNat16(toOptArray(mem_mgr.inner_.memory_buckets_.get(1 : MemoryId)), ?[1 : BucketId]); + + memory_0.write(0, [1, 2, 3]); + memory_0.write(0, [1, 2, 3]); + memory_1.write(0, [4, 5, 6]); + + var bytes = memory_0.read(0, 3); + test.equalsBytes(bytes, [1, 2, 3]); + + bytes := memory_1.read(0, 3); + test.equalsBytes(bytes, [4, 5, 6]); + + // + 1 is for the header. + test.equalsNat64(mem.size(), 2 * BUCKET_SIZE_IN_PAGES + 1); + }; + + func canBeReinitializedFromMemory(test: TestBuffer) { + let mem = Memory.VecMemory(); + var mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + var memory_0 = mem_mgr.get(0 : MemoryId); + var memory_1 = mem_mgr.get(1 : MemoryId); + + test.equalsInt64(memory_0.grow(1), 0); + test.equalsInt64(memory_1.grow(1), 0); + + memory_0.write(0, [1, 2, 3]); + memory_1.write(0, [4, 5, 6]); + + mem_mgr := MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + memory_0 := mem_mgr.get(0 : MemoryId); + memory_1 := mem_mgr.get(1 : MemoryId); + + var bytes = memory_0.read(0, 3); + test.equalsBytes(bytes, [1, 2, 3]); + + bytes := memory_1.read(0, 3); + test.equalsBytes(bytes, [4, 5, 6]); + }; + + func growingSameMemoryMultipleTimesDoesntIncreaseUnderlyingAllocation(test: TestBuffer) { + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + + // Grow the memory by 1 page. This should increase the underlying allocation + // by `BUCKET_SIZE_IN_PAGES` pages. + test.equalsInt64(memory_0.grow(1), 0); + test.equalsNat64(mem.size(), 1 + BUCKET_SIZE_IN_PAGES); + + // Grow the memory again. This should NOT increase the underlying allocation. + test.equalsInt64(memory_0.grow(1), 1); + test.equalsNat64(memory_0.size(), 2); + test.equalsNat64(mem.size(), 1 + BUCKET_SIZE_IN_PAGES); + + // Grow the memory up to the BUCKET_SIZE_IN_PAGES. This should NOT increase the underlying + // allocation. + test.equalsInt64(memory_0.grow(BUCKET_SIZE_IN_PAGES - 2), 2); + test.equalsNat64(memory_0.size(), BUCKET_SIZE_IN_PAGES); + test.equalsNat64(mem.size(), 1 + BUCKET_SIZE_IN_PAGES); + + // Grow the memory by one more page. This should increase the underlying allocation. + test.equalsInt64(memory_0.grow(1), Int64.fromNat64(BUCKET_SIZE_IN_PAGES)); + test.equalsNat64(memory_0.size(), BUCKET_SIZE_IN_PAGES + 1); + test.equalsNat64(mem.size(), 1 + 2 * BUCKET_SIZE_IN_PAGES); + }; + + func doesNotGrowMemoryUnnecessarily(test: TestBuffer) { + let mem = Memory.VecMemory(); + let initial_size = BUCKET_SIZE_IN_PAGES * 2; + + // Grow the memory manually before passing it into the memory manager. + ignore mem.grow(initial_size); + + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + + // Grow the memory by 1 page. + test.equalsInt64(memory_0.grow(1), 0); + test.equalsNat64(mem.size(), initial_size); + + // Grow the memory by BUCKET_SIZE_IN_PAGES more pages, which will cause the underlying + // allocation to increase. + test.equalsInt64(memory_0.grow(BUCKET_SIZE_IN_PAGES), 1); + test.equalsNat64(mem.size(), 1 + BUCKET_SIZE_IN_PAGES * 2); + }; + + func growingBeyondCapacityFails(test: TestBuffer) { + let MAX_MEMORY_IN_PAGES: Nat64 = MemoryManager.MAX_NUM_BUCKETS * BUCKET_SIZE_IN_PAGES; + + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + + test.equalsInt64(memory_0.grow(MAX_MEMORY_IN_PAGES + 1), -1); + + // Try to grow the memory by MAX_MEMORY_IN_PAGES + 1. + test.equalsInt64(memory_0.grow(1), 0); // should succeed + test.equalsInt64(memory_0.grow(MAX_MEMORY_IN_PAGES), -1); // should fail. + }; + + func canWriteAcrossBucketBoundaries(test: TestBuffer) { + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + + test.equalsInt64(memory_0.grow(BUCKET_SIZE_IN_PAGES + 1), 0); + + memory_0.write( + mem_mgr.inner_.bucketSizeInBytes() - 1, + [1, 2, 3], + ); + + let bytes = memory_0.read( + mem_mgr.inner_.bucketSizeInBytes() - 1, + 3 + ); + test.equalsBytes(bytes, [1, 2, 3]); + }; + + func canWriteAcrossBucketBoundariesWithInterleavingMemories(test: TestBuffer) { + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + let memory_1 = mem_mgr.get(1 : MemoryId); + + test.equalsInt64(memory_0.grow(BUCKET_SIZE_IN_PAGES), 0); + test.equalsInt64(memory_1.grow(1), 0); + test.equalsInt64(memory_0.grow(1), Int64.fromNat64(BUCKET_SIZE_IN_PAGES)); + + memory_0.write( + mem_mgr.inner_.bucketSizeInBytes() - 1, + [1, 2, 3], + ); + memory_1.write(0, [4, 5, 6]); + + var bytes = memory_0.read(Constants.WASM_PAGE_SIZE * BUCKET_SIZE_IN_PAGES - 1, 3); + test.equalsBytes(bytes, [1, 2, 3]); + + bytes := memory_1.read(0, 3); + test.equalsBytes(bytes, [4, 5, 6]); + }; + + func readingOutOfBoundsShouldTrap(test: TestBuffer) { + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + let memory_1 = mem_mgr.get(1 : MemoryId); + + test.equalsInt64(memory_0.grow(1), 0); + test.equalsInt64(memory_1.grow(1), 0); + + let bytes = memory_0.read(0, Nat64.toNat(Constants.WASM_PAGE_SIZE) + 1); + }; + + func writingOutOfBoundsShouldTrap(test: TestBuffer) { + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + let memory_1 = mem_mgr.get(1 : MemoryId); + + test.equalsInt64(memory_0.grow(1), 0); + test.equalsInt64(memory_1.grow(1), 0); + + let bytes = Array.freeze(Array.init(Nat64.toNat(Constants.WASM_PAGE_SIZE) + 1, 0)); + memory_0.write(0, bytes); + }; + + func readingZeroBytesFromEmptyMemoryShouldNotTrap(test: TestBuffer) { + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + + test.equalsNat64(memory_0.size(), 0); + let bytes = memory_0.read(0, 0); + }; + + func writingZeroBytesToEmptyMemoryShouldNotTrap(test: TestBuffer) { + let mem = Memory.VecMemory(); + let mem_mgr = MemoryManager.initWithBuckets(mem, Nat16.fromNat(Nat64.toNat(BUCKET_SIZE_IN_PAGES))); + let memory_0 = mem_mgr.get(0 : MemoryId); + + test.equalsNat64(memory_0.size(), 0); + memory_0.write(0, []); + }; + + public func run() { + let test = TestableItems.TestBuffer(); + + canGetMemory(test); + canAllocateAndUseMemory(test); + canAllocateAndUseMultipleMemories(test); + canBeReinitializedFromMemory(test); + growingSameMemoryMultipleTimesDoesntIncreaseUnderlyingAllocation(test); + doesNotGrowMemoryUnnecessarily(test); + growingBeyondCapacityFails(test); + canWriteAcrossBucketBoundaries(test); + canWriteAcrossBucketBoundariesWithInterleavingMemories(test); + //readingOutOfBoundsShouldTrap(test); // @todo: succeed on trap + //writingOutOfBoundsShouldTrap(test); // @todo: succeed on trap + readingZeroBytesFromEmptyMemoryShouldNotTrap(test); + writingZeroBytesToEmptyMemoryShouldNotTrap(test); + + test.run("Test memory manager module"); + }; + +}; \ No newline at end of file diff --git a/test/stableBTreeTest/module/testableItems.mo b/test/stableBTreeTest/module/testableItems.mo new file mode 100644 index 00000000..f3a4b5ea --- /dev/null +++ b/test/stableBTreeTest/module/testableItems.mo @@ -0,0 +1,339 @@ +import Types "../../src/types"; + +import Suite "mo:matchers/Suite"; +import Matchers "mo:matchers/Matchers"; +import Testable "mo:matchers/Testable"; + +import Nat "mo:base/Nat"; +import Nat8 "mo:base/Nat8"; +import Nat16 "mo:base/Nat16"; +import Nat32 "mo:base/Nat32"; +import Nat64 "mo:base/Nat64"; +import Int "mo:base/Int"; +import Int8 "mo:base/Int8"; +import Int16 "mo:base/Int16"; +import Int32 "mo:base/Int32"; +import Int64 "mo:base/Int64"; +import Buffer "mo:base/Buffer"; +import Text "mo:base/Text"; +import Array "mo:base/Array"; +import Result "mo:base/Result"; +import Bool "mo:base/Bool"; + +module { + + // For convenience: from base module + type Result = Result.Result; + // For convenience: from matchers module + let { run;test;suite; } = Suite; + // For convenience: from types module + type InsertError = Types.InsertError; + type NodeType = Types.NodeType; + type Entry = Types.Entry; + + func testNat(item: Nat) : Testable.TestableItem { + { display = Nat.toText; equals = Nat.equal; item ; }; + }; + + func testNat8(item: Nat8) : Testable.TestableItem { + { display = Nat8.toText; equals = Nat8.equal; item ; }; + }; + + func testNat16(item: Nat16) : Testable.TestableItem { + { display = Nat16.toText; equals = Nat16.equal; item ; }; + }; + + func testNat32(item: Nat32) : Testable.TestableItem { + { display = Nat32.toText; equals = Nat32.equal; item ; }; + }; + + func testNat64(item: Nat64) : Testable.TestableItem { + { display = Nat64.toText; equals = Nat64.equal; item ; }; + }; + + func testInt(item: Int) : Testable.TestableItem { + { display = Int.toText; equals = Int.equal; item ; }; + }; + + func testInt8(item: Int8) : Testable.TestableItem { + { display = Int8.toText; equals = Int8.equal; item ; }; + }; + + func testInt16(item: Int16) : Testable.TestableItem { + { display = Int16.toText; equals = Int16.equal; item ; }; + }; + + func testInt32(item: Int32) : Testable.TestableItem { + { display = Int32.toText; equals = Int32.equal; item ; }; + }; + + func testInt64(item: Int64) : Testable.TestableItem { + { display = Int64.toText; equals = Int64.equal; item ; }; + }; + + func bytesToText(bytes: [Nat8]) : Text { + let text_buffer = Buffer.Buffer(bytes.size() + 2); + text_buffer.add("["); + for (byte in Array.vals(bytes)){ + text_buffer.add(Nat8.toText(byte) # " "); + }; + text_buffer.add("]"); + Text.join("", text_buffer.vals()); + }; + + func optBytesToText(bytes: ?[Nat8]) : Text { + optToText<[Nat8]>(bytes, bytesToText); + }; + + func optBytesEqual(bytes_1: ?[Nat8], bytes_2: ?[Nat8]) : Bool { + optEqual<[Nat8]>(bytes_1, bytes_2, bytesEqual); + }; + + func bytesEqual(bytes_1: [Nat8], bytes_2: [Nat8]) : Bool { + bytes_1 == bytes_2; + }; + + func testBytes(item: [Nat8]) : Testable.TestableItem<[Nat8]> { + { + display = bytesToText; + equals = bytesEqual; + item; + }; + }; + + func optToText(opt: ?T, to_text: T -> Text) : Text { + switch(opt){ + case(null) { "opt: null"; }; + case(?item) { "opt: " # to_text(item); }; + }; + }; + + func optEqual(opt1: ?T, opt2: ?T, equal: (T, T) -> Bool) : Bool { + switch(opt1){ + case(null) { + switch(opt2){ + case(null) { true; }; + case(_) { false; }; + }; + }; + case(?item1) { + switch(opt2){ + case(null) { false; }; + case(?item2) { equal(item1, item2); }; + }; + }; + }; + }; + + func testOptItem(opt: ?T, to_text: T -> Text, equal: (T, T) -> Bool) : Testable.TestableItem { + { + display = func(opt: ?T) : Text { + optToText(opt, to_text); + }; + equals = func (opt1: ?T, opt2: ?T) : Bool { + optEqual(opt1, opt2, equal) + }; + item = opt; + }; + }; + + func testResult( + item: Result, + ok_to_text: Ok -> Text, + err_to_text: Err -> Text, + ok_equal: (Ok, Ok) -> Bool, + err_equal: (Err, Err) -> Bool + ) : Testable.TestableItem> { + { + display = func(item: Result) : Text { + switch(item){ + case(#ok(ok)) { "ok: " # ok_to_text(ok); }; + case(#err(err)) { "err: " # err_to_text(err); }; + }; + }; + equals = func (r1: Result, r2: Result) : Bool { + Result.equal(ok_equal, err_equal, r1, r2); + }; + item; + }; + }; + + func testOptBytes(item: ?[Nat8]) : Testable.TestableItem { + testOptItem<[Nat8]>(item, bytesToText, bytesEqual); + }; + + func insertErrorToText(error: InsertError) : Text { + switch(error){ + case(#KeyTooLarge({given; max;})){ + "Key too large. Given is '" # Nat.toText(given) # "' while max is '" # Nat.toText(max) # "'"; + }; + case(#ValueTooLarge({given; max;})){ + "Value too large. Given is '" # Nat.toText(given) # "' while max is '" # Nat.toText(max) # "'"; + }; + }; + }; + + func insertErrorEqual(error1: InsertError, error2: InsertError) : Bool { + error1 == error2; + }; + + type InsertResult = Result; + + func testInsertResult(result: InsertResult) : Testable.TestableItem { + testResult(result, optBytesToText, insertErrorToText, optBytesEqual, insertErrorEqual); + }; + + func testNodeType(node_type: NodeType) : Testable.TestableItem { + { + display = func(node_type: NodeType) : Text { + switch(node_type){ + case(#Leaf){ "Leaf"; }; + case(#Internal){ "Internal"; }; + }; + }; + equals = func (type1: NodeType, type2: NodeType) : Bool { + type1 == type2; + }; + item = node_type; + }; + }; + + func testBool(bool: Bool) : Testable.TestableItem { + { + display = func(bool: Bool) : Text { + Bool.toText(bool); + }; + equals = func(bool1: Bool, bool2: Bool) : Bool { + bool1 == bool2; + }; + item = bool; + }; + }; + + func entryToText(entry: Entry) : Text { + "key: " # bytesToText(entry.0) # ", value: " # bytesToText(entry.1); + }; + + func entryEqual(entry1: Entry, entry2: Entry) : Bool { + bytesEqual(entry1.0, entry2.0) and bytesEqual(entry1.1, entry2.1); + }; + + func testEntries(entries: [Entry]) : Testable.TestableItem<[Entry]> { + { + display = func(entries: [Entry]) : Text { + let text_buffer = Buffer.Buffer(entries.size() + 2); + text_buffer.add("["); + for (entry in Array.vals(entries)){ + text_buffer.add("(" # entryToText(entry) # "), "); + }; + text_buffer.add("]"); + Text.join("", text_buffer.vals()); + }; + equals = func(entries1: [Entry], entries2: [Entry]) : Bool { + Array.equal(entries1, entries2, entryEqual); + }; + item = entries; + }; + }; + + func arrayNat16toText(array: [Nat16]) : Text { + let text_buffer = Buffer.Buffer(array.size() + 2); + text_buffer.add("["); + for (elem in Array.vals(array)){ + text_buffer.add("(" # Nat16.toText(elem) # "), "); + }; + text_buffer.add("]"); + Text.join("", text_buffer.vals()); + }; + + func arrayNat16Equal(array1: [Nat16], array2: [Nat16]) : Bool { + Array.equal(array1, array2, Nat16.equal); + }; + + func testOptArrayNat16(array: ?[Nat16]) : Testable.TestableItem { + testOptItem<[Nat16]>(array, arrayNat16toText, arrayNat16Equal); + }; + + public class TestBuffer() { + + let tests_ = Buffer.Buffer(0); + + public func run(message: Text) { + Suite.run(suite(message, tests_.toArray())); + }; + + public func equals(message: Text, actual: T, expected: Testable.TestableItem){ + tests_.add(test(message, actual, Matchers.equals(expected))); + }; + + public func equalsNat(actual: Nat, expected: Nat){ + equals("equalsNat", actual, testNat(expected)); + }; + + public func equalsNat8(actual: Nat8, expected: Nat8){ + equals("equalsNat8", actual, testNat8(expected)); + }; + + public func equalsNat16(actual: Nat16, expected: Nat16){ + equals("equalsNat16", actual, testNat16(expected)); + }; + + public func equalsNat32(actual: Nat32, expected: Nat32){ + equals("equalsNat32", actual, testNat32(expected)); + }; + + public func equalsNat64(actual: Nat64, expected: Nat64){ + equals("equalsNat64", actual, testNat64(expected)); + }; + + public func equalsInt(actual: Int, expected: Int){ + equals("equalsInt", actual, testInt(expected)); + }; + + public func equalsInt8(actual: Int8, expected: Int8){ + equals("equalsInt8", actual, testInt8(expected)); + }; + + public func equalsInt16(actual: Int16, expected: Int16){ + equals("equalsInt16", actual, testInt16(expected)); + }; + + public func equalsInt32(actual: Int32, expected: Int32){ + equals("equalsInt32", actual, testInt32(expected)); + }; + + public func equalsInt64(actual: Int64, expected: Int64){ + equals("equalsInt64", actual, testInt64(expected)); + }; + + public func equalsBytes(actual: [Nat8], expected: [Nat8]){ + equals<[Nat8]>("equalsBytes", actual, testBytes(expected)); + }; + + public func equalsOptBytes(actual: ?[Nat8], expected: ?[Nat8]){ + equals("equalsOptBytes", actual, testOptBytes(expected)); + }; + + public func equalsInsertResult(actual: InsertResult, expected: InsertResult){ + equals("equalsInsertResult", actual, testInsertResult(expected)); + }; + + public func equalsNodeType(actual: NodeType, expected: NodeType){ + equals("equalsNodeType", actual, testNodeType(expected)); + }; + + public func equalsBool(actual: Bool, expected: Bool){ + equals("equalsBool", actual, testBool(expected)); + }; + + public func equalsEntries(actual: [Entry], expected: [Entry]){ + equals<[Entry]>("equalsEntries", actual, testEntries(expected)); + }; + + public func equalsOptArrayNat16(actual: ?[Nat16], expected: ?[Nat16]){ + equals("equalsOptArrayNat16", actual, testOptArrayNat16(expected)); + }; + + }; + +}; \ No newline at end of file diff --git a/test/stableBTreeTest/module/vessel.dhall b/test/stableBTreeTest/module/vessel.dhall new file mode 100644 index 00000000..5dedcbdd --- /dev/null +++ b/test/stableBTreeTest/module/vessel.dhall @@ -0,0 +1,4 @@ +let mainVessel = ../../vessel.dhall + +in mainVessel + with dependencies = mainVessel.dependencies # [ "matchers" ]