diff --git a/stew/arraybuf.nim b/stew/arraybuf.nim new file mode 100644 index 0000000..df1e409 --- /dev/null +++ b/stew/arraybuf.nim @@ -0,0 +1,104 @@ +# stew +# Copyright 2024 Status Research & Development GmbH +# Licensed under either of +# +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +# +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import ./[evalonce, arrayops] + +type ArrayBuf*[N: static int, T] = object + ## An fixed-capacity, allocation-free buffer with a seq-like API - suitable + ## for keeping small amounts of data since the full capacity is reserved on + ## instantiation (using an `array`). + # + # `N` must be "simple enough" or one of these will trigger + # TODO https://github.com/nim-lang/Nim/issues/24043 + # TODO https://github.com/nim-lang/Nim/issues/24044 + # TODO https://github.com/nim-lang/Nim/issues/24045 + buf*: array[N, T] + + when sizeof(int) > sizeof(uint8): + when N <= int(uint8.high): + n*: uint8 + else: + when sizeof(int) > sizeof(uint16): + when N <= int(uint16.high): + n*: uint16 + else: + when sizeof(int) > sizeof(uint32): + # TODO https://github.com/nim-lang/Nim/issues/24041 + when N <= cast[int](uint32.high): + n*: uint32 + else: + n*: int + else: + n*: int + else: + n*: int + else: + n*: int + # Number of entries actually in use - uses the smallest unsigned integer + # that can hold values up to the capacity to avoid wasting memory on + # alignment and counting, specially when `T = byte` and odd sizes are used + +template len*(b: ArrayBuf): int = + int(b.n) + +template setLen*(b: var ArrayBuf, newLenParam: int) = + newLenParam.evalOnceAs(newLen) + let nl = typeof(b.n)(newLen) + for i in newLen ..< b.len(): + reset(b.buf[b.len() - i - 1]) # reset cleared items when shrinking + b.n = nl + +template data*(bParam: ArrayBuf): openArray = + bParam.evalOnceAs(b) + b.buf.toOpenArray(0, b.len() - 1) + +template data*(bParam: var ArrayBuf): var openArray = + bParam.evalOnceAs(b) + b.buf.toOpenArray(0, b.len() - 1) + +iterator items*[N, T](b: ArrayBuf[N, T]): lent T = + for i in 0 ..< b.len: + yield b.d[i] + +iterator mitems*[N, T](b: var ArrayBuf[N, T]): var T = + for i in 0 ..< b.len: + yield b.d[i] + +iterator pairs*[N, T](b: ArrayBuf[N, T]): (int, lent T) = + for i in 0 ..< b.len: + yield (i, b.buf[i]) + +template `[]`*[N, T](b: ArrayBuf[N, T], i: int): lent T = + b.buf[i] + +template `[]`*[N, T](b: var ArrayBuf[N, T], i: int): var T = + b.buf[i] + +template `[]=`*[N, T](b: var ArrayBuf[N, T], i: int, v: T) = + b.buf[i] = v + +template `==`*(a, b: ArrayBuf): bool = + a.data() == b.data() + +template `<`*(a, b: ArrayBuf): bool = + a.data() < b.data() + +template add*[N, T](b: var ArrayBuf[N, T], v: T) = + ## Adds items up to capacity then drops the rest + # TODO `b` is evaluated multiple times but since it's a `var` this should + # _hopefully_ be fine.. + if b.len < N: + b.buf[b.len] = v + b.n += 1 + +template add*[N, T](b: var ArrayBuf[N, T], v: openArray[T]) = + ## Adds items up to capacity then drops the rest + # TODO `b` is evaluated multiple times but since it's a `var` this should + # _hopefully_ be fine.. + b.n += typeof(b.n)(b.buf.toOpenArray(b.len, N - 1).copyFrom(v)) diff --git a/stew/evalonce.nim b/stew/evalonce.nim new file mode 100644 index 0000000..4664381 --- /dev/null +++ b/stew/evalonce.nim @@ -0,0 +1,42 @@ +# stew +# Copyright 2024 Status Research & Development GmbH +# Licensed under either of +# +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +# +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import std/macros + +macro evalOnceAs*(exp, alias: untyped): untyped = + ## Ensure that `exp` is evaluated only once unless it is a symbol in which + ## case it's used directly. + ## + ## A common case where this is useful is template parameters which, when + ## an expression is passed in, get evaluated multiple times. + ## + ## Based on a similar macro in std/sequtils + expectKind(alias, nnkIdent) + + let + body = nnkStmtList.newTree() + val = + if exp.kind == nnkSym: + # The symbol can be used directly + # TODO dot expressions? etc.. + exp + else: + let val = genSym(ident = "evalOnce_" & $alias) + body.add newLetStmt(val, exp) + val + body.add( + newProc( + name = genSym(nskTemplate, $alias), + params = [getType(untyped)], + body = val, + procType = nnkTemplateDef, + ) + ) + + body diff --git a/tests/all_tests.nim b/tests/all_tests.nim index d6e76ad..b62837a 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -11,6 +11,7 @@ import ranges/all import test_assign2, + test_arraybuf, test_arrayops, test_base10, test_base32, diff --git a/tests/test_arraybuf.nim b/tests/test_arraybuf.nim new file mode 100644 index 0000000..ec4cf30 --- /dev/null +++ b/tests/test_arraybuf.nim @@ -0,0 +1,36 @@ +# stew +# Copyright 2024 Status Research & Development GmbH +# Licensed under either of +# +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +# +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +{.used.} + +import + ../stew/arraybuf, + unittest2 + +suite "ArrayBuf": + test "single evaluation": + var v: byte = 0 + proc f(): ArrayBuf[33, byte] = + v += 1 + result.add v + + # check doesn't support `openArray` (!) + doAssert f().data() == [byte 1] + + test "overflow add": + var v: ArrayBuf[2, byte] + v.add(byte 0) + doAssert v.data() == [byte 0] + + v.add([byte 1, 2, 3]) + + doAssert v.data() == [byte 0, 1] + + v.add(byte 4) + doAssert v.data() == [byte 0, 1]