diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5337ca49..afe28417 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: # Remember to update me in package-set.yml as well env: vessel_version: "v0.7.0" - moc_version: "0.9.8" + moc_version: "0.10.0" jobs: tests: @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - run: npm ci - name: "initial checks" run: npm run validate diff --git a/.github/workflows/package-set.yml b/.github/workflows/package-set.yml index fa567bb9..e548f6ee 100644 --- a/.github/workflows/package-set.yml +++ b/.github/workflows/package-set.yml @@ -8,7 +8,7 @@ on: env: vessel_version: "v0.7.0" - moc_version: "0.9.8" + moc_version: "0.10.0" jobs: verify: diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index e3ff0c2d..95bd9186 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -14,7 +14,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-22.04] - node: [16] + node: [18] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node }} diff --git a/src/ExperimentalStableMemory.mo b/src/ExperimentalStableMemory.mo index e6bda4da..f5099948 100644 --- a/src/ExperimentalStableMemory.mo +++ b/src/ExperimentalStableMemory.mo @@ -4,6 +4,11 @@ /// and may be replaced by safer alternatives in later versions of Motoko. /// Use at your own risk and discretion. /// +/// **DEPRECATION**: Use of `ExperimentalStableMemory` library may be deprecated in future. +/// Going forward, users should consider using library `Region.mo` to allocate *isolated* regions of memory instead. +/// Using dedicated regions for different user applications ensures that writing +/// to one region will not affect the state of another, unrelated region. +/// /// This is a lightweight abstraction over IC _stable memory_ and supports persisting /// raw binary data across Motoko upgrades. /// Use of this module is fully compatible with Motoko's use of @@ -29,7 +34,7 @@ /// page size reported by Motoko function `size()`. /// This (and the cap on growth) are to accommodate Motoko's stable variables. /// Applications that plan to use Motoko stable variables sparingly or not at all can -/// increase `--max-stable-pages` as desired, approaching the IC maximum (initially 8GiB, then 32Gib, currently 48Gib). +/// increase `--max-stable-pages` as desired, approaching the IC maximum (initially 8GiB, then 32Gib, currently 64Gib). /// All applications should reserve at least one page for stable variable data, even when no stable variables are used. /// /// Usage: diff --git a/src/Region.mo b/src/Region.mo new file mode 100644 index 00000000..ba460d23 --- /dev/null +++ b/src/Region.mo @@ -0,0 +1,376 @@ +/// Byte-level access to isolated, (virtual) stable memory _regions_. +/// +/// This is a moderately lightweight abstraction over IC _stable memory_ and supports persisting +/// regions of binary data across Motoko upgrades. +/// Use of this module is fully compatible with Motoko's use of +/// _stable variables_, whose persistence mechanism also uses (real) IC stable memory internally, but does not interfere with this API. +/// It is also fully compatible with existing uses of the `ExperimentalStableMemory` library, which has a similar interface, but, +/// only supported a single memory region, without isolation between different applications. +/// +/// Memory is allocated, using `grow(region, pages)`, sequentially and on demand, in units of 64KiB logical pages, starting with 0 allocated pages. +/// New pages are zero initialized. +/// Growth is capped by a soft limit on physical page count controlled by compile-time flag +/// `--max-stable-pages ` (the default is 65536, or 4GiB). +/// +/// Each `load` operation loads from region relative byte address `offset` in little-endian +/// format using the natural bit-width of the type in question. +/// The operation traps if attempting to read beyond the current region size. +/// +/// Each `store` operation stores to region relative byte address `offset` in little-endian format using the natural bit-width of the type in question. +/// The operation traps if attempting to write beyond the current region size. +/// +/// Text values can be handled by using `Text.decodeUtf8` and `Text.encodeUtf8`, in conjunction with `loadBlob` and `storeBlob`. +/// +/// The current region allocation and region contents are preserved across upgrades. +/// +/// NB: The IC's actual stable memory size (`ic0.stable_size`) may exceed the +/// total page size reported by summing all regions sizes. +/// This (and the cap on growth) are to accommodate Motoko's stable variables and bookkeeping for regions. +/// Applications that plan to use Motoko stable variables sparingly or not at all can +/// increase `--max-stable-pages` as desired, approaching the IC maximum (initially 8GiB, then 32Gib, currently 64Gib). +/// All applications should reserve at least one page for stable variable data, even when no stable variables are used. +/// +/// Usage: +/// ```motoko no-repl +/// import Region "mo:base/Region"; +/// ``` + +import Prim "mo:⛔"; + +module { + + /// A stateful handle to an isolated region of IC stable memory. + /// `Region` is a stable type and regions can be stored in stable variables. + public type Region = Prim.Types.Region; + + /// Allocate a new, isolated Region of size 0. + /// + /// Example: + /// + /// ```motoko no-repl + /// let region = Region.new(); + /// assert Region.size(region) == 0; + /// ``` + public let new : () -> Region = Prim.regionNew; + + /// Return a Nat identifying the given region. + /// Maybe be used for equality, comparison and hashing. + /// NB: Regions returned by `new()` are numbered from 16 + /// (regions 0..15 are currently reserved for internal use). + /// Allocate a new, isolated Region of size 0. + /// + /// Example: + /// + /// ```motoko no-repl + /// let region = Region.new(); + /// assert Region.id(region) == 16; + /// ``` + public let id : Region -> Nat = Prim.regionId; + + /// Current size of `region`, in pages. + /// Each page is 64KiB (65536 bytes). + /// Initially `0`. + /// Preserved across upgrades, together with contents of allocated + /// stable memory. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let beforeSize = Region.size(region); + /// ignore Region.grow(region, 10); + /// let afterSize = Region.size(region); + /// afterSize - beforeSize // => 10 + /// ``` + public let size : (region : Region) -> (pages : Nat64) = Prim.regionSize; + + /// Grow current `size` of `region` by the given number of pages. + /// Each page is 64KiB (65536 bytes). + /// Returns the previous `size` when able to grow. + /// Returns `0xFFFF_FFFF_FFFF_FFFF` if remaining pages insufficient. + /// Every new page is zero-initialized, containing byte 0x00 at every offset. + /// Function `grow` is capped by a soft limit on `size` controlled by compile-time flag + /// `--max-stable-pages ` (the default is 65536, or 4GiB). + /// + /// Example: + /// ```motoko no-repl + /// import Error "mo:base/Error"; + /// + /// let region = Region.new(); + /// let beforeSize = Region.grow(region, 10); + /// if (beforeSize == 0xFFFF_FFFF_FFFF_FFFF) { + /// throw Error.reject("Out of memory"); + /// }; + /// let afterSize = Region.size(region); + /// afterSize - beforeSize // => 10 + /// ``` + public let grow : (region : Region, newPages : Nat64) -> (oldPages : Nat64) = Prim.regionGrow; + + + /// Within `region`, load a `Nat8` value from `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeNat8(region, offset, value); + /// Region.loadNat8(region, offset) // => 123 + /// ``` + public let loadNat8 : (region : Region, offset : Nat64) -> Nat8 = Prim.regionLoadNat8; + + /// Within `region`, store a `Nat8` value at `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeNat8(region, offset, value); + /// Region.loadNat8(region, offset) // => 123 + /// ``` + public let storeNat8 : (region : Region, offset : Nat64, value : Nat8) -> () = Prim.regionStoreNat8; + + /// Within `region`, load a `Nat16` value from `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeNat16(region, offset, value); + /// Region.loadNat16(region, offset) // => 123 + /// ``` + public let loadNat16 : (region : Region, offset : Nat64) -> Nat16 = Prim.regionLoadNat16; + + /// Within `region`, store a `Nat16` value at `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeNat16(region, offset, value); + /// Region.loadNat16(region, offset) // => 123 + /// ``` + public let storeNat16 : (region : Region, offset : Nat64, value : Nat16) -> () = Prim.regionStoreNat16; + + /// Within `region`, load a `Nat32` value from `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeNat32(region, offset, value); + /// Region.loadNat32(region, offset) // => 123 + /// ``` + public let loadNat32 : (region : Region, offset : Nat64) -> Nat32 = Prim.regionLoadNat32; + + /// Within `region`, store a `Nat32` value at `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeNat32(region, offset, value); + /// Region.loadNat32(region, offset) // => 123 + /// ``` + public let storeNat32 : (region : Region, offset : Nat64, value : Nat32) -> () = Prim.regionStoreNat32; + + /// Within `region`, load a `Nat64` value from `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeNat64(region, offset, value); + /// Region.loadNat64(region, offset) // => 123 + /// ``` + public let loadNat64 : (region : Region, offset : Nat64) -> Nat64 = Prim.regionLoadNat64; + + /// Within `region`, store a `Nat64` value at `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeNat64(region, offset, value); + /// Region.loadNat64(region, offset) // => 123 + /// ``` + public let storeNat64 : (region : Region, offset : Nat64, value : Nat64) -> () = Prim.regionStoreNat64; + + /// Within `region`, load a `Int8` value from `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeInt8(region, offset, value); + /// Region.loadInt8(region, offset) // => 123 + /// ``` + public let loadInt8 : (region : Region, offset : Nat64) -> Int8 = Prim.regionLoadInt8; + + /// Within `region`, store a `Int8` value at `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeInt8(region, offset, value); + /// Region.loadInt8(region, offset) // => 123 + /// ``` + public let storeInt8 : (region : Region, offset : Nat64, value : Int8) -> () = Prim.regionStoreInt8; + + /// Within `region`, load a `Int16` value from `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeInt16(region, offset, value); + /// Region.loadInt16(region, offset) // => 123 + /// ``` + public let loadInt16 : (region : Region, offset : Nat64) -> Int16 = Prim.regionLoadInt16; + + /// Within `region`, store a `Int16` value at `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeInt16(region, offset, value); + /// Region.loadInt16(region, offset) // => 123 + /// ``` + public let storeInt16 : (region : Region, offset : Nat64, value : Int16) -> () = Prim.regionStoreInt16; + + /// Within `region`, load a `Int32` value from `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeInt32(region, offset, value); + /// Region.loadInt32(region, offset) // => 123 + /// ``` + public let loadInt32 : (region : Region, offset : Nat64) -> Int32 = Prim.regionLoadInt32; + + /// Within `region`, store a `Int32` value at `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeInt32(region, offset, value); + /// Region.loadInt32(region, offset) // => 123 + /// ``` + public let storeInt32 : (region : Region, offset : Nat64, value : Int32) -> () = Prim.regionStoreInt32; + + /// Within `region`, load a `Int64` value from `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeInt64(region, offset, value); + /// Region.loadInt64(region, offset) // => 123 + /// ``` + public let loadInt64 : (region : Region, offset : Nat64) -> Int64 = Prim.regionLoadInt64; + + /// Within `region`, store a `Int64` value at `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 123; + /// Region.storeInt64(region, offset, value); + /// Region.loadInt64(region, offset) // => 123 + /// ``` + public let storeInt64 : (region : Region, offset : Nat64, value : Int64) -> () = Prim.regionStoreInt64; + + + /// Within `region`, loads a `Float` value from the given `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 1.25; + /// Region.storeFloat(region, offset, value); + /// Region.loadFloat(region, offset) // => 1.25 + /// ``` + public let loadFloat : (region : Region, offset : Nat64) -> Float = Prim.regionLoadFloat; + + /// Within `region`, store float `value` at the given `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// let region = Region.new(); + /// let offset = 0; + /// let value = 1.25; + /// Region.storeFloat(region, offset, value); + /// Region.loadFloat(region, offset) // => 1.25 + /// ``` + public let storeFloat : (region: Region, offset : Nat64, value : Float) -> () = Prim.regionStoreFloat; + + /// Within `region,` load `size` bytes starting from `offset` as a `Blob`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// import Blob "mo:base/Blob"; + /// + /// let region = Region.new(); + /// let offset = 0; + /// let value = Blob.fromArray([1, 2, 3]); + /// let size = value.size(); + /// Region.storeBlob(region, offset, value); + /// Blob.toArray(Region.loadBlob(region, offset, size)) // => [1, 2, 3] + /// ``` + public let loadBlob : (region : Region, offset : Nat64, size : Nat) -> Blob = Prim.regionLoadBlob; + + /// Within `region, write `blob.size()` bytes of `blob` beginning at `offset`. + /// Traps on an out-of-bounds access. + /// + /// Example: + /// ```motoko no-repl + /// import Blob "mo:base/Blob"; + /// + /// let region = Region.new(); + /// let offset = 0; + /// let value = Blob.fromArray([1, 2, 3]); + /// let size = value.size(); + /// Region.storeBlob(region, offset, value); + /// Blob.toArray(Region.loadBlob(region, offset, size)) // => [1, 2, 3] + /// ``` + public let storeBlob : (region : Region, offset : Nat64, value : Blob) -> () = Prim.regionStoreBlob; + +}