From ee730658eeeb3a3be66025ba9b5d56b7cee9f735 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 2 May 2025 20:58:27 -0600 Subject: [PATCH 1/7] adding len type param --- stack_strings.nim | 148 +++++++++++++++++++++++++--------------------- tests/t_basic.nim | 66 ++++++++++++++++++++- 2 files changed, 144 insertions(+), 70 deletions(-) diff --git a/stack_strings.nim b/stack_strings.nim index 6fbd511..dd62720 100644 --- a/stack_strings.nim +++ b/stack_strings.nim @@ -4,7 +4,7 @@ The `stack_strings` module provides a string implementation that works with 100% This module is primarily meant for programs that want to avoid any and all heap allocation, such as code for embedded targets. If you use `--mm:arc` and `-d:useMalloc` in tandem with this module, your program will be able to do string operations without allocating any memory at runtime. -# The `StackString` Type +# The `StackStringBase` Type The [StackString] type is an object with a fixed size buffer and an integer to store its current length. It works very similarly to `string`, but its internal buffer cannot be resized, and must be known at compile time. @@ -20,7 +20,7 @@ runnableExamples: Note the lack of `new` in the name; there is no runtime allocation going on here. -Under the hood, a `StackString[10]` object was created and returned, and its length was set to `0`. +Under the hood, a `StackStringBase[10]` object was created and returned, and its length was set to `0`. Since buffers are fixed-size and known at compile time, the capacity of the [StackString] is encoded as part of its type. You can add to a [StackString], assuming it has capacity: @@ -45,11 +45,11 @@ If you have a static string (such as a string literal), you can use the [ss] pro runnableExamples: let greeting = ss"Hello, world!" - doAssert greeting is StackString[13] + doAssert greeting is StackStringBase[13] ##[ The resulting [StackString]'s capacity will be the length of the static string provided. -In the case of the code above, the type of `greeting` is `StackString[13]`. +In the case of the code above, the type of `greeting` is `StackStringBase[13]`. If you have a runtime string (or anything else that's covered by the [IndexableChars] union type) that you want to convert to a [StackString], you can use [toStackString]: ]## @@ -63,7 +63,7 @@ runnableExamples: See also: [unsafeToStackString], [tryToStackString], [toStackStringTruncate]. -# Manipulating `StackString` objects +# Manipulating `StackStringBase` objects In Nim, `string` is mutable if it is stored in a `var`, as opposed to a `let`. The same applies to [StackString]. @@ -151,20 +151,31 @@ template raiseInsufficientCapacityDefect(msg: string, capacity: Natural, request raise newInsufficientCapacityDefect(msg, capacity, requestedCapacity) -type StackString*[Size: static Natural] = object - ## A stack-allocated string with a fixed capacity +import std/macros + +template checkLenType(lenType, size: typed) = + when high(lenType) < size: + {.error: "stack_strings: LenType is too small to store the string length".} - lenInternal: Natural - ## The current string length +template defaultLenType(): auto = + Natural + +type StackStringBase*[T; Size: static Natural] = object + ## A stack-allocated string with a fixed capacity + + lenInternal: T ## The current string length data*: array[Size + 1, char] ## The underlying string data. ## If you just want to iterate over the string's characters, use the [items] iterator. -type IndexableChars* = cstring | string | openArray[char] | StackString + +type StackString*[Size: static Natural] = StackStringBase[Natural, Size] + +type IndexableChars* = cstring | string | openArray[char] | StackStringBase ## Indexable data types that contain chars -func toString*(this: StackString): string = +func toString*(this: StackStringBase): string = ## Allocates a new string with the content of the provided [StackString]. ## Note that this will allocate heap memory and copy the [StackString]'s content. ## @@ -179,9 +190,9 @@ func toString*(this: StackString): string = for c in this.items: result.add(c) -func `$`*(this: StackString): string {.inline.} = +func `$`*(this: StackStringBase): string {.inline.} = ## Converts the [StackString] to a `string`. - ## Note that this proc allocates a new string and copies the contents of the StackString into the newly created string. + ## Note that this proc allocates a new string and copies the contents of the StackStringBase into the newly created string. ## ## See [warnOnStackStringDollar] and [fatalOnStackStringDollar] for information about compiler warnings errors this may cause. ## If you want to avoid any warnings or errors specific to this proc, use [toString] instead (which is intentionally more explicit). @@ -189,7 +200,7 @@ func `$`*(this: StackString): string {.inline.} = when stackStringsPreventAllocation: {.fatal: "The `$` proc can allocate memory at runtime, see `stackStringsPreventAllocation`".} - const errMsg {.used.} = "Conversion of StackString to string with `$` proc. If this was intentional, use `toString` instead." + const errMsg {.used.} = "Conversion of StackStringBase to string with `$` proc. If this was intentional, use `toString` instead." when fatalOnStackStringDollar: {.fatal: errMsg.} when warnOnStackStringDollar: @@ -197,38 +208,40 @@ func `$`*(this: StackString): string {.inline.} = return this.toString() -func ss*(str: static string): static auto = +func ss*(str: static string, lenType: typedesc = defaultLenType()): static auto = ## Creates a [StackString] object from a static string. ## The [StackString]'s capacity will be the string's actual length. runnableExamples: let name = ss"John Doe" - doAssert name is StackString[8] + doAssert name is StackStringBase[8] var data: array[str.len + 1, char] for i in 0 ..< str.len: data[i] = str[i] - return StackString[str.len](lenInternal: str.len, data: data) + checkLenType(lenType, str.len) + return StackStringBase[lenType, str.len](lenInternal: str.len, data: data) -func stackStringOfCap*(capacity: static Natural): static auto = +func stackStringOfCap*(capacity: static Natural, lenType: typedesc = defaultLenType()): static auto = ## Creates a [StackString] with the specified capacity. ## This proc does not allocate heap memory. runnableExamples: var str = stackStringOfCap(10) - doAssert str is StackString[10] + doAssert str is StackStringBase[10] doAssert str.len == 0 - return StackString[capacity](lenInternal: 0, data: array[capacity + 1, char].default) + checkLenType(lenType, capacity) + return StackStringBase[lenType, capacity](lenInternal: 0, data: array[capacity + 1, char].default) -func len*(this: StackString): Natural {.inline.} = +func len*(this: StackStringBase): Natural {.inline.} = ## The current string length return this.lenInternal -func high*(this: StackString): int {.inline.} = +func high*(this: StackStringBase): int {.inline.} = ## Returns the highest index of the [StackString], or `-1` if it is empty runnableExamples: var str1 = "Hello world" @@ -239,7 +252,7 @@ func high*(this: StackString): int {.inline.} = return this.len - 1 -func capacity*(this: StackString): Natural {.inline.} = +func capacity*(this: StackStringBase): Natural {.inline.} = ## Returns the capacity of the [StackString] runnableExamples: var ssLit = ss"Same capacity" @@ -254,7 +267,7 @@ func capacity*(this: StackString): Natural {.inline.} = return this.data.len - 1 -iterator items*(this: StackString): char {.inline.} = +iterator items*(this: StackStringBase): char {.inline.} = ## Iterates over each char in the [StackString] runnableExamples: let str = ss"abc" @@ -272,7 +285,7 @@ iterator items*(this: StackString): char {.inline.} = yield this.data[i] inc i -iterator mitems*(this: var StackString): var char {.inline.} = +iterator mitems*(this: var StackStringBase): var char {.inline.} = ## Iterates over each char in the [StackString], returning a mutable reference runnableExamples: var str = ss"fly in the sky" @@ -291,7 +304,7 @@ iterator mitems*(this: var StackString): var char {.inline.} = yield this.data[i] inc i -iterator pairs*(this: StackString): (int, char) {.inline.} = +iterator pairs*(this: StackStringBase): (int, char) {.inline.} = ## Iterates over each index-char pairs in the [StackString] runnableExamples: let str = ss"abc" @@ -310,7 +323,7 @@ iterator pairs*(this: StackString): (int, char) {.inline.} = yield (i, this.data[i]) inc i -iterator mpairs*(this: var StackString): (int, var char) {.inline.} = +iterator mpairs*(this: var StackStringBase): (int, var char) {.inline.} = ## Iterates over each index-char pairs in the [StackString], returning a mutable reference to the char runnableExamples: var str = ss"ooo" @@ -328,7 +341,7 @@ iterator mpairs*(this: var StackString): (int, var char) {.inline.} = inc i {.boundChecks: off.} -func `[]`*(this: StackString, i: Natural | BackwardsIndex): char {.inline, raises: [IndexDefect].} = +func `[]`*(this: StackStringBase, i: Natural | BackwardsIndex): char {.inline, raises: [IndexDefect].} = ## Returns the character at the specified index in the [StackString], or raises `IndexDefect` if the index is invalid runnableExamples: let str = ss"Hello world" @@ -343,7 +356,7 @@ func `[]`*(this: StackString, i: Natural | BackwardsIndex): char {.inline, raise else: i - # Do bounds check manually because the StackString's len field is the actual bound we want to check, not data.len + # Do bounds check manually because the StackStringBase's len field is the actual bound we want to check, not data.len when not defined(danger): let cond = idx >= this.len or idx < 0 @@ -361,7 +374,7 @@ func `[]`*(this: StackString, i: Natural | BackwardsIndex): char {.inline, raise return this.data[idx] {.boundChecks: on.} -template `[]`*(this: StackString, slice: HSlice): openArray[char] = +template `[]`*(this: StackStringBase, slice: HSlice): openArray[char] = ## Returns an `openArray` for the specified range in the [StackString], or raises `RangeDefect` if the range is invalid. ## The returned range is a reference to the original [StackString] `data` memory. ## @@ -386,7 +399,7 @@ template `[]`*(this: StackString, slice: HSlice): openArray[char] = let a = slice.a let b = slice.b - # Do bounds check manually because the StackString's len field is the actual bound we want to check, not data.len + # Do bounds check manually because the StackStringBase's len field is the actual bound we want to check, not data.len when not defined(danger): let cond = ( a > b or @@ -403,7 +416,7 @@ template `[]`*(this: StackString, slice: HSlice): openArray[char] = this.data.toOpenArray(a, b) {.boundChecks: off.} -func tryGet*(this: StackString, i: Natural | BackwardsIndex): Option[char] = +func tryGet*(this: StackStringBase, i: Natural | BackwardsIndex): Option[char] = ## Returns the character at the specified index in the [StackString], or returns `None` if the index is invalid runnableExamples: import std/options @@ -430,7 +443,7 @@ func tryGet*(this: StackString, i: Natural | BackwardsIndex): Option[char] = {.boundChecks: on.} {.boundChecks: off.} -func unsafeGet*(this: StackString, i: Natural | BackwardsIndex): char {.inline.} = +func unsafeGet*(this: StackStringBase, i: Natural | BackwardsIndex): char {.inline.} = ## Returns the character at the specified index in the [StackString]. ## ## Performs no bounds checks whatsoever; use only if you're 100% sure your index won't extend beyond the [StackString]'s capacity + its nil terminator. @@ -445,7 +458,7 @@ func unsafeGet*(this: StackString, i: Natural | BackwardsIndex): char {.inline.} {.boundChecks: on.} {.boundChecks: off.} -func `[]=`*(this: var StackString, i: Natural | BackwardsIndex, value: char) {.inline, raises: [IndexDefect].} = +func `[]=`*(this: var StackStringBase, i: Natural | BackwardsIndex, value: char) {.inline, raises: [IndexDefect].} = ## Sets the character at the specified index in the [StackString], or raises `IndexDefect` if the index is invalid runnableExamples: var str = ss"Hello world" @@ -461,7 +474,7 @@ func `[]=`*(this: var StackString, i: Natural | BackwardsIndex, value: char) {.i else: i - # Do bounds check manually because the StackString's len field is the actual bound we want to check, not data.len + # Do bounds check manually because the StackStringBase's len field is the actual bound we want to check, not data.len when not defined(danger): let cond = idx >= this.len or idx < 0 @@ -480,7 +493,7 @@ func `[]=`*(this: var StackString, i: Natural | BackwardsIndex, value: char) {.i {.boundChecks: on.} {.boundChecks: off.} -func trySet*(this: var StackString, i: Natural | BackwardsIndex, value: char): bool = +func trySet*(this: var StackStringBase, i: Natural | BackwardsIndex, value: char): bool = ## Sets the character at the specified index in the [StackString] and returns true, or returns false if the index is invalid let idx = when i is BackwardsIndex: @@ -496,7 +509,7 @@ func trySet*(this: var StackString, i: Natural | BackwardsIndex, value: char): b {.boundChecks: on.} {.boundChecks: off.} -func unsafeSet*(this: var StackString, i: Natural | BackwardsIndex, value: char) {.inline.} = +func unsafeSet*(this: var StackStringBase, i: Natural | BackwardsIndex, value: char) {.inline.} = ## Sets the character at the specified index in the [StackString]. ## ## Performs no bounds checks whatsoever; use only if you're 100% sure your index won't extend beyond the [StackString]'s capacity. @@ -512,7 +525,7 @@ func unsafeSet*(this: var StackString, i: Natural | BackwardsIndex, value: char) # Bound checks are unnecessary here because the length is checked first {.boundChecks: off.} -func `==`*(this: StackString, str: IndexableChars): bool {.inline.} = +func `==`*(this: StackStringBase, str: IndexableChars): bool {.inline.} = ## Returns whether the [StackString]'s content is equal to the content of another set of characters runnableExamples: let str1 = ss"abc" @@ -538,7 +551,7 @@ func `==`*(this: StackString, str: IndexableChars): bool {.inline.} = {.boundChecks: on.} {.boundChecks: off.} -proc unsafeAdd*(this: var StackString, strOrChar: auto) {.inline.} = +proc unsafeAdd*(this: var StackStringBase, strOrChar: auto) {.inline.} = ## Appends the value to the [StackString]. ## No capacity checks are performed whatsoever; only use this when you are 100% sure there is enough capacity! runnableExamples: @@ -557,11 +570,11 @@ proc unsafeAdd*(this: var StackString, strOrChar: auto) {.inline.} = for i in this.len ..< newLen: this.data[i] = strOrChar[i - this.len] - this.lenInternal = newLen + this.lenInternal = typeof(this.lenInternal)(newLen) {.boundChecks: on.} {.boundChecks: off.} -proc tryAdd*(this: var StackString, strOrChar: auto): bool {.inline.} = +proc tryAdd*(this: var StackStringBase, strOrChar: auto): bool {.inline.} = ## Appends the value to the [StackString]. ## If there is enough capacity to accomodate the new value, true will be returned. ## If there is not enough capacity to accomodate the new value, false will be returned. @@ -594,9 +607,9 @@ proc tryAdd*(this: var StackString, strOrChar: auto): bool {.inline.} = {.boundChecks: on.} {.boundChecks: off.} -proc addTruncate*(this: var StackString, strOrChar: auto): bool {.inline, discardable.} = +proc addTruncate*(this: var StackStringBase, strOrChar: auto): bool {.inline, discardable.} = ## Appends the provided value to the [StackString]. - ## If the capacity of the StackString is not enough to accomodate the value, the chars that cannot be appended will be truncated. + ## If the capacity of the StackStringBase is not enough to accomodate the value, the chars that cannot be appended will be truncated. ## If the provided value is truncated, `false` will be returned. Otherwise, `true` will be returned. ## ## If you want to use a version that raises an exception when there is not enough, you can use [add] instead. @@ -636,10 +649,10 @@ proc addTruncate*(this: var StackString, strOrChar: auto): bool {.inline, discar for i in this.len ..< newLen: this.data[i] = strOrChar[i - this.len] - this.lenInternal = newLen + this.lenInternal = typeof(this.lenInternal)(newLen) {.boundChecks: on.} -proc add*(this: var StackString, strOrChar: auto) {.inline, raises: [InsufficientCapacityDefect].} = +proc add*(this: var StackStringBase, strOrChar: auto) {.inline, raises: [InsufficientCapacityDefect].} = ## Appends the provided value to the [StackString]. ## If there is not enough capacity to accomodate the new value, [InsufficientCapacityDefect] will be raised. ## @@ -668,12 +681,12 @@ proc add*(this: var StackString, strOrChar: auto) {.inline, raises: [Insufficien else: this.len + strOrChar.len raiseInsufficientCapacityDefect( - "Cannot append to StackString due to insufficient capacity (capacity: " & $this.capacity & ", required capacity: " & $reqCap & ")", + "Cannot append to StackStringBase due to insufficient capacity (capacity: " & $this.capacity & ", required capacity: " & $reqCap & ")", this.capacity, reqCap, ) {.boundChecks: off.} -proc unsafeSetLen*(this: var StackString, newLen: Natural | BackwardsIndex, writeZerosOnTruncate: bool = true) {.inline.} = +proc unsafeSetLen*(this: var StackStringBase, newLen: Natural | BackwardsIndex, writeZerosOnTruncate: bool = true) {.inline.} = ## Sets the length of the [StackString] to `newLen`. ## No capacity checks are performed whatsoever; only use this if you're 100% sure you are not exceeding capacity! ## @@ -692,10 +705,10 @@ proc unsafeSetLen*(this: var StackString, newLen: Natural | BackwardsIndex, writ for i in lenRes ..< this.len: this.data[i] = '\x00' - this.lenInternal = lenRes + this.lenInternal = typeof(this.lenInternal)(lenRes) {.boundChecks: on.} -proc trySetLen*(this: var StackString, newLen: Natural | BackwardsIndex, writeZerosOnTruncate: bool = true): bool {.inline.} = +proc trySetLen*(this: var StackStringBase, newLen: Natural | BackwardsIndex, writeZerosOnTruncate: bool = true): bool {.inline.} = ## Sets the length of the [StackString] to `newLen`, then returns true. ## If `newLen` is more than the [StackString]'s capacity, `false` will be returned. ## @@ -720,7 +733,7 @@ proc trySetLen*(this: var StackString, newLen: Natural | BackwardsIndex, writeZe return true -proc setLen*(this: var StackString, newLen: Natural | BackwardsIndex, writeZerosOnTruncate: bool = true) {.inline, raises: [InsufficientCapacityDefect].} = +proc setLen*(this: var StackStringBase, newLen: Natural | BackwardsIndex, writeZerosOnTruncate: bool = true) {.inline, raises: [InsufficientCapacityDefect].} = ## Sets the length of the [StackString] to `newLen`. ## If `newLen` is more than the [StackString]'s capacity, [InsufficientCapacityDefect] will be raised. ## @@ -762,7 +775,7 @@ proc setLen*(this: var StackString, newLen: Natural | BackwardsIndex, writeZeros this.unsafeSetLen(newLen, writeZerosOnTruncate) -func find*(this: StackString, c: char): int {.inline.} = +func find*(this: StackStringBase, c: char): int {.inline.} = ## Finds the index of the specified char in the [StackString], or returns `-1` if the char was not found runnableExamples: let str = ss"abcdef" @@ -777,7 +790,7 @@ func find*(this: StackString, c: char): int {.inline.} = return -1 -func contains*(this: StackString, c: char): bool {.inline.} = +func contains*(this: StackStringBase, c: char): bool {.inline.} = ## Returns whether the specified char can be found within the [StackString] runnableExamples: let str = ss"abcdef" @@ -788,7 +801,7 @@ func contains*(this: StackString, c: char): bool {.inline.} = return this.find(c) != -1 -func find*(this: StackString, substr: StackString | string | IndexableChars): int {.inline.} = +func find*(this: StackStringBase, substr: StackStringBase | string | IndexableChars): int {.inline.} = ## Finds the index of the specified substring in the [StackString], or returns `-1` if the substring was not found runnableExamples: let str = ss"abcdef" @@ -814,7 +827,7 @@ func find*(this: StackString, substr: StackString | string | IndexableChars): in # Didn't already return index, so no match was found return -1 -func contains*(this: StackString, substr: StackString | string | IndexableChars): bool {.inline.} = +func contains*(this: StackStringBase, substr: StackStringBase | string | IndexableChars): bool {.inline.} = ## Returns whether the specified substring can be found within the [StackString] runnableExamples: let str = ss"abcdef" @@ -827,13 +840,13 @@ func contains*(this: StackString, substr: StackString | string | IndexableChars) return this.find(substr) != -1 -template toOpenArray*(this: StackString): untyped = +template toOpenArray*(this: StackStringBase): untyped = ## Converts the [StackString] to `openArray[char]`. ## Thanks to ElegantBeef for help on this template. this.data.toOpenArray(0, this.high) -template toCstring*(this: StackString): cstring = +template toCstring*(this: StackStringBase): cstring = ## Converts the [StackString] to `cstring`. ## Note that no memory copying is done; this simply casts the [StackString]'s `data` to cstring. ## @@ -852,9 +865,9 @@ template toCstring*(this: StackString): cstring = when NimMajor > 1: ## Nim 2.0 no longer requires `unsafeaddr` cast[cstring](addr this.data[0]) else: - cast[cstring](unsafeaddr this.data[0]) + cast[cstring](unsafeAddr this.data[0]) -proc toHeapCstring*(this: StackString): cstring {.inline.} = +proc toHeapCstring*(this: StackStringBase): cstring {.inline.} = ## Allocates a `cstring` on the heap and copies the contents of the [StackString] into it. ## The `cstring` is a pointer to heap memory which must be freed manually by the caller using `dealloc`. ## If you just want to get the [StackString]'s `data` stack pointer as a `cstring`, use `toCstring` instead. @@ -881,10 +894,10 @@ proc toHeapCstring*(this: StackString): cstring {.inline.} = when NimMajor > 1: ## Nim 2.0 no longer requires `unsafeaddr` moveMem(result, addr this.data[0], len) else: - moveMem(result, unsafeaddr this.data[0], len) + moveMem(result, unsafeAddr this.data[0], len) result[len] = '\x00' -proc unsafeToStackString*(content: IndexableChars, size: static Natural): StackString[size] {.inline.} = +proc unsafeToStackString*(content: IndexableChars, size: static Natural, lenType: typedesc = defaultLenType()): auto {.inline.} = ## Creates a new [StackString] of the specified size using the provided content. ## No capacity checks are performed whatsoever; only use this when you are 100% sure that the content's length is less than or equal to the specified size! runnableExamples: @@ -900,11 +913,10 @@ proc unsafeToStackString*(content: IndexableChars, size: static Natural): StackS doAssert stackStr.len == nimStr.len - - result = stackStringOfCap(size) + result = stackStringOfCap(size, lenType) result.unsafeAdd(content) -proc toStackString*(content: IndexableChars, size: static Natural): StackString[size] {.inline.} = +proc toStackString*[T; N: static int](content: IndexableChars, size: static Natural): StackStringBase[T, N] {.inline.} = ## Creates a new [StackString] of the specified size using the provided content. ## If you don't want to raise a defect when the input string exceeds the specified size, use [tryToStackString]. ## If you want to truncate the content in the resulting [StackString] if it's too long, use [toStackStringTruncate]. @@ -926,11 +938,11 @@ proc toStackString*(content: IndexableChars, size: static Natural): StackString[ let len = content.len if len > size: - raise newInsufficientCapacityDefect("Tried to create a StackString of size " & $size & ", but the provided content was of size " & $len, size, len) + raise newInsufficientCapacityDefect("Tried to create a StackStringBase of size " & $size & ", but the provided content was of size " & $len, size, len) return content.unsafeToStackString(size) -proc tryToStackString*(content: IndexableChars, size: static Natural): Option[StackString[size]] {.inline.} = +proc tryToStackString*[T; N: static int](content: IndexableChars, size: static Natural): Option[StackStringBase[T, N]] {.inline.} = ## Creates a new [StackString] of the specified size using the provided content. ## If the content's length is more than the `size` argument, then None will be returned. ## If you want to raise a defect when the input string exceeds the specified size, use [toStackString]. @@ -948,11 +960,11 @@ proc tryToStackString*(content: IndexableChars, size: static Natural): Option[St doAssert stackStrRes2.isSome if content.len > size: - return none[StackString[size]]() + return none[StackStringBase[size]]() return some content.unsafeToStackString(size) -proc toStackStringTruncate*(content: IndexableChars, size: static Natural): StackString[size] {.inline.} = +proc toStackStringTruncate*(content: IndexableChars, size: static Natural, lenType: typedesc = defaultLenType()): auto {.inline.} = ## Creates a new [StackString] of the specified size using the provided content. ## If the content length is more than `size`, only the part of the content that can fit in the size will be included, and the rest will be truncated. runnableExamples: @@ -961,5 +973,5 @@ proc toStackStringTruncate*(content: IndexableChars, size: static Natural): Stac doAssert stackStr == "Hello" - result = stackStringOfCap(size) + result = stackStringOfCap(size, lenType) result.addTruncate(content) diff --git a/tests/t_basic.nim b/tests/t_basic.nim index c1f99de..22bb50b 100644 --- a/tests/t_basic.nim +++ b/tests/t_basic.nim @@ -1,4 +1,5 @@ -import unittest +import std/unittest +import std/strutils import stack_strings @@ -9,16 +10,41 @@ test "Can add": check str == "lol" +test "LenType compiletime check": + check compiles(block: + const strMsg1 = "abcd".repeat(31) + var str1 = ss(strMsg1, uint8) + echo str1.data + ) + check not compiles(block: + const strMsg2 = "abcd".repeat(32) + var str2 = ss(strMsg2, int8) + echo str2.data + ) + check compiles(block: + var str3 = stackStringOfCap(127, int8) + str3.add(ss"lol") + ) + check not compiles(block: + var str3 = stackStringOfCap(128, int8) + str3.add(ss"lol") + ) + test "Random tests": - var str1 = ss"Hello world" + var str1: StackString[11] = ss"Hello world" var str2 = ss"Hi world" var str3 = ss"abc" + var str4 = ss("hello world", uint8) # The string will be truncated, and the truncated data will be overwritten with zeros str1.unsafeSetLen(5) check str1 == "Hello" check str1.data == ['H', 'e', 'l', 'l', 'o', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00'] + str4.unsafeSetLen(5) + # check str4 == "hello" + check str4.data == ['h', 'e', 'l', 'l', 'o', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00'] + # If we're sure it's safe to skip overwriting the truncated data with zeros, we can disable it str2.unsafeSetLen(2, writeZerosOnTruncate = false) check str2 == "Hi" @@ -37,3 +63,39 @@ test "Random tests": check str1.unsafeGet(5) == 'a' discard $str1 + str4.unsafeSet(5, 'a') + check str4.unsafeGet(5) == 'a' + discard $str4 + +test "len type overrides": + let nimStr = "hi" + var str6 = stackStringOfCap(10, int8) + str6.addTruncate(nimStr) + check str6.len == 2 + + echo $str6 + +test "unsafeToStackString": + let nimStr = "hi" + var str7 = nimStr.unsafeToStackString(10) + check str7.len == 2 + check str7 == "hi" + + check not compiles(block: + var str8 = nimStr.unsafeToStackString(200, int8) + check str8.len == 2 + check str8 == "hi" + ) + +test "toStackStringTruncate": + let nimStr = "hi" + var str9 = nimStr.toStackStringTruncate(10) + check str9.len == 2 + check str9 == "hi" + + check not compiles(block: + var str10 = nimStr.toStackStringTruncate(200, int8) + check str10.len == 2 + check str10 == "hi" + ) + From f351c5d2af4e6bd5aab04e1275d7de688c2ce46d Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 2 May 2025 21:07:18 -0600 Subject: [PATCH 2/7] fixing size check --- stack_strings.nim | 5 +++-- tests/t_basic.nim | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/stack_strings.nim b/stack_strings.nim index dd62720..8839c35 100644 --- a/stack_strings.nim +++ b/stack_strings.nim @@ -154,14 +154,14 @@ template raiseInsufficientCapacityDefect(msg: string, capacity: Natural, request import std/macros template checkLenType(lenType, size: typed) = - when high(lenType) < size: + when high(lenType) < size + 1: {.error: "stack_strings: LenType is too small to store the string length".} template defaultLenType(): auto = Natural type StackStringBase*[T; Size: static Natural] = object - ## A stack-allocated string with a fixed capacity + ## A stack-allocated string with a fixed capacity with a length type of `T` lenInternal: T ## The current string length @@ -171,6 +171,7 @@ type StackStringBase*[T; Size: static Natural] = object type StackString*[Size: static Natural] = StackStringBase[Natural, Size] + ## A stack-allocated string with a fixed capacity type IndexableChars* = cstring | string | openArray[char] | StackStringBase ## Indexable data types that contain chars diff --git a/tests/t_basic.nim b/tests/t_basic.nim index 22bb50b..ac4f1aa 100644 --- a/tests/t_basic.nim +++ b/tests/t_basic.nim @@ -22,11 +22,11 @@ test "LenType compiletime check": echo str2.data ) check compiles(block: - var str3 = stackStringOfCap(127, int8) + var str3 = stackStringOfCap(126, int8) str3.add(ss"lol") ) check not compiles(block: - var str3 = stackStringOfCap(128, int8) + var str3 = stackStringOfCap(127, int8) str3.add(ss"lol") ) From f7c5e5ee303afb7119418866bf35fd1a55a17b9a Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 2 May 2025 21:21:50 -0600 Subject: [PATCH 3/7] add more compile time length checks --- stack_strings.nim | 44 +++++++++++++++++++++++++++++++++++--------- tests/t_basic.nim | 14 ++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/stack_strings.nim b/stack_strings.nim index 8839c35..2a372a0 100644 --- a/stack_strings.nim +++ b/stack_strings.nim @@ -153,7 +153,7 @@ template raiseInsufficientCapacityDefect(msg: string, capacity: Natural, request import std/macros -template checkLenType(lenType, size: typed) = +template checkCompileTimeLenType(lenType, size: typed) = when high(lenType) < size + 1: {.error: "stack_strings: LenType is too small to store the string length".} @@ -222,7 +222,7 @@ func ss*(str: static string, lenType: typedesc = defaultLenType()): static auto for i in 0 ..< str.len: data[i] = str[i] - checkLenType(lenType, str.len) + checkCompileTimeLenType(lenType, str.len) return StackStringBase[lenType, str.len](lenInternal: str.len, data: data) func stackStringOfCap*(capacity: static Natural, lenType: typedesc = defaultLenType()): static auto = @@ -234,12 +234,12 @@ func stackStringOfCap*(capacity: static Natural, lenType: typedesc = defaultLenT doAssert str is StackStringBase[10] doAssert str.len == 0 - checkLenType(lenType, capacity) + checkCompileTimeLenType(lenType, capacity) return StackStringBase[lenType, capacity](lenInternal: 0, data: array[capacity + 1, char].default) func len*(this: StackStringBase): Natural {.inline.} = ## The current string length - + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) return this.lenInternal func high*(this: StackStringBase): int {.inline.} = @@ -266,6 +266,8 @@ func capacity*(this: StackStringBase): Natural {.inline.} = doAssert extraCap.capacity == 10 doAssert extraCap.len == 2 + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + return this.data.len - 1 iterator items*(this: StackStringBase): char {.inline.} = @@ -467,6 +469,8 @@ func `[]=`*(this: var StackStringBase, i: Natural | BackwardsIndex, value: char) str[0] = 'Y' doAssert str == "Yello world" + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + when stackStringsPreventAllocation: {.fatal: "The `[]=` proc can allocate memory at runtime, see `stackStringsPreventAllocation`".} @@ -497,6 +501,8 @@ func `[]=`*(this: var StackStringBase, i: Natural | BackwardsIndex, value: char) func trySet*(this: var StackStringBase, i: Natural | BackwardsIndex, value: char): bool = ## Sets the character at the specified index in the [StackString] and returns true, or returns false if the index is invalid + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + let idx = when i is BackwardsIndex: this.len - i.int else: @@ -562,6 +568,8 @@ proc unsafeAdd*(this: var StackStringBase, strOrChar: auto) {.inline.} = bigCap.unsafeAdd(strToAdd) + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + when strOrChar is char: this.data[this.len] = strOrChar inc this.lenInternal @@ -594,6 +602,8 @@ proc tryAdd*(this: var StackStringBase, strOrChar: auto): bool {.inline.} = doAssert bigCap == "Hello" doAssert smallCap == "" + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + let newLen = when strOrChar is char: this.len + 1 else: @@ -627,6 +637,8 @@ proc addTruncate*(this: var StackStringBase, strOrChar: auto): bool {.inline, di doAssert bigCap == "Hello" doAssert smallCap == "Hel" + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + when strOrChar is char: if this.len >= this.capacity: return false @@ -673,6 +685,8 @@ proc add*(this: var StackStringBase, strOrChar: auto) {.inline, raises: [Insuffi bigCap.add('!') doAssert bigCap == "Hello!" + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + when defined(danger): this.unsafeAdd(strOrChar) else: @@ -693,6 +707,8 @@ proc unsafeSetLen*(this: var StackStringBase, newLen: Natural | BackwardsIndex, ## ## If `writeZerosOnTruncate` is true and `newLen` is less than the current capacity, the truncated bytes will be zeroed out. + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + let lenRes = when newLen is BackwardsIndex: this.len - newLen.int else: @@ -721,6 +737,8 @@ proc trySetLen*(this: var StackStringBase, newLen: Natural | BackwardsIndex, wri doAssert str1.trySetLen(11) == true doAssert str1.trySetLen(12) == false + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + when not defined(danger): let lenRes = when newLen is BackwardsIndex: this.len - newLen.int @@ -760,6 +778,8 @@ proc setLen*(this: var StackStringBase, newLen: Natural | BackwardsIndex, writeZ str3.setLen(^1) doAssert str3 == "ab" + checkCompileTimeLenType(this.lenInternal.type, this.data.len - 1) + when stackStringsPreventAllocation: {.fatal: "The `setLen` proc can allocate memory at runtime, see `stackStringsPreventAllocation`".} @@ -917,7 +937,7 @@ proc unsafeToStackString*(content: IndexableChars, size: static Natural, lenType result = stackStringOfCap(size, lenType) result.unsafeAdd(content) -proc toStackString*[T; N: static int](content: IndexableChars, size: static Natural): StackStringBase[T, N] {.inline.} = +proc toStackString*(content: IndexableChars, size: static Natural, lenType: typedesc = defaultLenType()): StackStringBase[lenType, size] {.inline.} = ## Creates a new [StackString] of the specified size using the provided content. ## If you don't want to raise a defect when the input string exceeds the specified size, use [tryToStackString]. ## If you want to truncate the content in the resulting [StackString] if it's too long, use [toStackStringTruncate]. @@ -933,7 +953,6 @@ proc toStackString*[T; N: static int](content: IndexableChars, size: static Natu doAssertRaises InsufficientCapacityDefect, stackStr.add(", and everyone in it!") - when stackStringsPreventAllocation: {.fatal: "The `toStackString` proc can allocate memory at runtime, see `stackStringsPreventAllocation`".} @@ -941,9 +960,11 @@ proc toStackString*[T; N: static int](content: IndexableChars, size: static Natu if len > size: raise newInsufficientCapacityDefect("Tried to create a StackStringBase of size " & $size & ", but the provided content was of size " & $len, size, len) + checkCompileTimeLenType(lenType, size) + return content.unsafeToStackString(size) -proc tryToStackString*[T; N: static int](content: IndexableChars, size: static Natural): Option[StackStringBase[T, N]] {.inline.} = +proc tryToStackString*(content: IndexableChars, size: static Natural, lenType: typedesc = defaultLenType()): Option[StackStringBase[lenType, size]] {.inline.} = ## Creates a new [StackString] of the specified size using the provided content. ## If the content's length is more than the `size` argument, then None will be returned. ## If you want to raise a defect when the input string exceeds the specified size, use [toStackString]. @@ -960,12 +981,14 @@ proc tryToStackString*[T; N: static int](content: IndexableChars, size: static N doAssert stackStrRes2.isSome + checkCompileTimeLenType(lenType, size) + if content.len > size: - return none[StackStringBase[size]]() + return none[StackStringBase[lenType, size]]() return some content.unsafeToStackString(size) -proc toStackStringTruncate*(content: IndexableChars, size: static Natural, lenType: typedesc = defaultLenType()): auto {.inline.} = +proc toStackStringTruncate*(content: IndexableChars, size: static Natural, lenType: typedesc = defaultLenType()): StackStringBase[lenType, size] {.inline.} = ## Creates a new [StackString] of the specified size using the provided content. ## If the content length is more than `size`, only the part of the content that can fit in the size will be included, and the rest will be truncated. runnableExamples: @@ -974,5 +997,8 @@ proc toStackStringTruncate*(content: IndexableChars, size: static Natural, lenTy doAssert stackStr == "Hello" + checkCompileTimeLenType(lenType, size) + result = stackStringOfCap(size, lenType) result.addTruncate(content) + diff --git a/tests/t_basic.nim b/tests/t_basic.nim index ac4f1aa..f536802 100644 --- a/tests/t_basic.nim +++ b/tests/t_basic.nim @@ -1,4 +1,5 @@ import std/unittest +import std/options import std/strutils import stack_strings @@ -87,6 +88,19 @@ test "unsafeToStackString": check str8 == "hi" ) +test "toStackString": + let nimStr = "hi" + var str9 = nimStr.toStackString(10) + check str9.len == 2 + check str9 == "hi" + +test "tryToStackString": + let nimStr = "hi" + var str9 = nimStr.tryToStackString(10) + check str9.isSome() + check str9.get().len == 2 + check str9.get() == "hi" + test "toStackStringTruncate": let nimStr = "hi" var str9 = nimStr.toStackStringTruncate(10) From 41654d35acad6668ffda58b388933cc2dec54bd4 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 2 May 2025 21:25:06 -0600 Subject: [PATCH 4/7] update docs --- stack_strings.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stack_strings.nim b/stack_strings.nim index 2a372a0..11eea23 100644 --- a/stack_strings.nim +++ b/stack_strings.nim @@ -4,7 +4,7 @@ The `stack_strings` module provides a string implementation that works with 100% This module is primarily meant for programs that want to avoid any and all heap allocation, such as code for embedded targets. If you use `--mm:arc` and `-d:useMalloc` in tandem with this module, your program will be able to do string operations without allocating any memory at runtime. -# The `StackStringBase` Type +# The `StackString` Type The [StackString] type is an object with a fixed size buffer and an integer to store its current length. It works very similarly to `string`, but its internal buffer cannot be resized, and must be known at compile time. @@ -20,7 +20,7 @@ runnableExamples: Note the lack of `new` in the name; there is no runtime allocation going on here. -Under the hood, a `StackStringBase[10]` object was created and returned, and its length was set to `0`. +Under the hood, a `StackString[10]` object was created and returned, and its length was set to `0`. Since buffers are fixed-size and known at compile time, the capacity of the [StackString] is encoded as part of its type. You can add to a [StackString], assuming it has capacity: @@ -49,7 +49,7 @@ runnableExamples: ##[ The resulting [StackString]'s capacity will be the length of the static string provided. -In the case of the code above, the type of `greeting` is `StackStringBase[13]`. +In the case of the code above, the type of `greeting` is `StackString[13]`. If you have a runtime string (or anything else that's covered by the [IndexableChars] union type) that you want to convert to a [StackString], you can use [toStackString]: ]## @@ -63,7 +63,7 @@ runnableExamples: See also: [unsafeToStackString], [tryToStackString], [toStackStringTruncate]. -# Manipulating `StackStringBase` objects +# Manipulating `StackString` objects In Nim, `string` is mutable if it is stored in a `var`, as opposed to a `let`. The same applies to [StackString]. From 192dcf9ee760baa92470ff44a0aecf35fe852453 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 2 May 2025 21:25:47 -0600 Subject: [PATCH 5/7] update docs --- stack_strings.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack_strings.nim b/stack_strings.nim index 11eea23..9d361ab 100644 --- a/stack_strings.nim +++ b/stack_strings.nim @@ -45,7 +45,7 @@ If you have a static string (such as a string literal), you can use the [ss] pro runnableExamples: let greeting = ss"Hello, world!" - doAssert greeting is StackStringBase[13] + doAssert greeting is StackString[13] ##[ The resulting [StackString]'s capacity will be the length of the static string provided. From 6ced963dc22d8e0dad60b68586c70ca498c89be7 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 2 May 2025 21:27:58 -0600 Subject: [PATCH 6/7] update docs --- stack_strings.nim | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/stack_strings.nim b/stack_strings.nim index 9d361ab..2b9248a 100644 --- a/stack_strings.nim +++ b/stack_strings.nim @@ -151,8 +151,6 @@ template raiseInsufficientCapacityDefect(msg: string, capacity: Natural, request raise newInsufficientCapacityDefect(msg, capacity, requestedCapacity) -import std/macros - template checkCompileTimeLenType(lenType, size: typed) = when high(lenType) < size + 1: {.error: "stack_strings: LenType is too small to store the string length".} @@ -215,7 +213,7 @@ func ss*(str: static string, lenType: typedesc = defaultLenType()): static auto runnableExamples: let name = ss"John Doe" - doAssert name is StackStringBase[8] + doAssert name is StackString[8] var data: array[str.len + 1, char] @@ -231,7 +229,7 @@ func stackStringOfCap*(capacity: static Natural, lenType: typedesc = defaultLenT runnableExamples: var str = stackStringOfCap(10) - doAssert str is StackStringBase[10] + doAssert str is StackString[10] doAssert str.len == 0 checkCompileTimeLenType(lenType, capacity) From 8bcffa0fb626a8c3a329af063c9c4f61c7e964e9 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 2 May 2025 21:32:03 -0600 Subject: [PATCH 7/7] update docs --- stack_strings.nim | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/stack_strings.nim b/stack_strings.nim index 2b9248a..ba84020 100644 --- a/stack_strings.nim +++ b/stack_strings.nim @@ -191,7 +191,7 @@ func toString*(this: StackStringBase): string = func `$`*(this: StackStringBase): string {.inline.} = ## Converts the [StackString] to a `string`. - ## Note that this proc allocates a new string and copies the contents of the StackStringBase into the newly created string. + ## Note that this proc allocates a new string and copies the contents of the StackString into the newly created string. ## ## See [warnOnStackStringDollar] and [fatalOnStackStringDollar] for information about compiler warnings errors this may cause. ## If you want to avoid any warnings or errors specific to this proc, use [toString] instead (which is intentionally more explicit). @@ -199,7 +199,7 @@ func `$`*(this: StackStringBase): string {.inline.} = when stackStringsPreventAllocation: {.fatal: "The `$` proc can allocate memory at runtime, see `stackStringsPreventAllocation`".} - const errMsg {.used.} = "Conversion of StackStringBase to string with `$` proc. If this was intentional, use `toString` instead." + const errMsg {.used.} = "Conversion of StackString to string with `$` proc. If this was intentional, use `toString` instead." when fatalOnStackStringDollar: {.fatal: errMsg.} when warnOnStackStringDollar: @@ -357,7 +357,7 @@ func `[]`*(this: StackStringBase, i: Natural | BackwardsIndex): char {.inline, r else: i - # Do bounds check manually because the StackStringBase's len field is the actual bound we want to check, not data.len + # Do bounds check manually because the StackString's len field is the actual bound we want to check, not data.len when not defined(danger): let cond = idx >= this.len or idx < 0 @@ -400,7 +400,7 @@ template `[]`*(this: StackStringBase, slice: HSlice): openArray[char] = let a = slice.a let b = slice.b - # Do bounds check manually because the StackStringBase's len field is the actual bound we want to check, not data.len + # Do bounds check manually because the StackString's len field is the actual bound we want to check, not data.len when not defined(danger): let cond = ( a > b or @@ -477,7 +477,7 @@ func `[]=`*(this: var StackStringBase, i: Natural | BackwardsIndex, value: char) else: i - # Do bounds check manually because the StackStringBase's len field is the actual bound we want to check, not data.len + # Do bounds check manually because the StackString's len field is the actual bound we want to check, not data.len when not defined(danger): let cond = idx >= this.len or idx < 0 @@ -618,7 +618,7 @@ proc tryAdd*(this: var StackStringBase, strOrChar: auto): bool {.inline.} = {.boundChecks: off.} proc addTruncate*(this: var StackStringBase, strOrChar: auto): bool {.inline, discardable.} = ## Appends the provided value to the [StackString]. - ## If the capacity of the StackStringBase is not enough to accomodate the value, the chars that cannot be appended will be truncated. + ## If the capacity of the StackString is not enough to accomodate the value, the chars that cannot be appended will be truncated. ## If the provided value is truncated, `false` will be returned. Otherwise, `true` will be returned. ## ## If you want to use a version that raises an exception when there is not enough, you can use [add] instead. @@ -694,7 +694,7 @@ proc add*(this: var StackStringBase, strOrChar: auto) {.inline, raises: [Insuffi else: this.len + strOrChar.len raiseInsufficientCapacityDefect( - "Cannot append to StackStringBase due to insufficient capacity (capacity: " & $this.capacity & ", required capacity: " & $reqCap & ")", + "Cannot append to StackString due to insufficient capacity (capacity: " & $this.capacity & ", required capacity: " & $reqCap & ")", this.capacity, reqCap, ) @@ -956,7 +956,7 @@ proc toStackString*(content: IndexableChars, size: static Natural, lenType: type let len = content.len if len > size: - raise newInsufficientCapacityDefect("Tried to create a StackStringBase of size " & $size & ", but the provided content was of size " & $len, size, len) + raise newInsufficientCapacityDefect("Tried to create a StackString of size " & $size & ", but the provided content was of size " & $len, size, len) checkCompileTimeLenType(lenType, size)