Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve deque, tables, and set modules #13620

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 74 additions & 39 deletions lib/pure/collections/deques.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@
## An implementation of a `deque`:idx: (double-ended queue).
## The underlying implementation uses a `seq`.
##
## Note that none of the procs that get an individual value from the deque should be used
## on an empty deque.
## If compiled with the `boundChecks` option, those procs will raise an `IndexDefect`
## on such access. This should not be relied upon, as `-d:danger` or `--checks:off` will
## disable those checks and then the procs may return garbage or crash the program.
## Deques are implicitly initialised as empty, similar to tables and seqs. But
PMunch marked this conversation as resolved.
Show resolved Hide resolved
## trying get an individual value from the deque will result in an `IndexError`
PMunch marked this conversation as resolved.
Show resolved Hide resolved
## if compiled with `boundChecks` turned on. Compiling without this option (or
## with `-d:danger` which disables it) may return garbage or crash the program.
##
## As such, a check to see if the deque is empty is needed before any
## access, unless your program logic guarantees it indirectly.

##
runnableExamples:
var a = [10, 20, 30, 40].toDeque

Expand Down Expand Up @@ -50,59 +49,78 @@ runnableExamples:

import std/private/since

import math
import math, hashes
PMunch marked this conversation as resolved.
Show resolved Hide resolved

type
Deque*[T] = object
## A double-ended queue backed with a ringed `seq` buffer.
##
## To initialize an empty deque,
## use the `initDeque proc <#initDeque,int>`_.
## To initialize an empty deque with a given capacity use
## `initDeque proc <#initDeque,int>`_.
data: seq[T]
head, tail, count, mask: int

const
defaultInitialSize* = 4
nimDequeDefaultInitialCapacity* {.intdefine.} = 4

template initImpl(result: typed, initialSize: int) =
let correctSize = nextPowerOfTwo(initialSize)
result.mask = correctSize - 1
newSeq(result.data, correctSize)
template initImpl(result: typed, initialCapacity: int) =
assert isPowerOfTwo(initialCapacity)
result.mask = initialCapacity-1
PMunch marked this conversation as resolved.
Show resolved Hide resolved
newSeq(result.data, initialCapacity)

template checkIfInitialized(deq: typed) =
when compiles(defaultInitialSize):
when declared(nimDequeDefaultInitialCapacity):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-exists. But why this line is needed? It seems unnecessary. See also nim-works/nimskull#161 (comment)

if deq.mask == 0:
initImpl(deq, defaultInitialSize)
initImpl(deq, nimDequeDefaultInitialCapacity)

proc initDeque*[T](initialSize: int = defaultInitialSize): Deque[T] =
## Creates a new empty deque.
proc initDeque*[T](initialCapacity: int = nimDequeDefaultInitialCapacity): Deque[T] =
## Create a new empty deque with a given capacity. An implicitly defined
## deque will have a capacity of 0 and be grown to fit elements on the first
## data insertion.
##
## Optionally, the initial capacity can be reserved via `initialSize`
## as a performance optimization
## (default: `defaultInitialSize <#defaultInitialSize>`_).
## The length of a newly created deque will still be 0.
## Optionally, the initial capacity can be reserved via `initialCapacity`
## as a performance optimization. The length of a newly created deque will
## still be 0.
##
## ``initialCapacity`` must be a power of two (default: 4).
PMunch marked this conversation as resolved.
Show resolved Hide resolved
## If you need to accept runtime values for this you could use the
## `nextPowerOfTwo proc<math.html#nextPowerOfTwo,int>`_ from the
## `math module<math.html>`_.
##
## **See also:**
## * `toDeque proc <#toDeque,openArray[T]>`_
result.initImpl(initialSize)
result.initImpl(initialCapacity)

proc len*[T](deq: Deque[T]): int {.inline.} =
## Return the number of elements in the `deq`.
PMunch marked this conversation as resolved.
Show resolved Hide resolved
result = deq.count

template high*[T](deq: Deque[T]): int =
deq.len - 1

proc toDeque*[T](x: openArray[T]): Deque[T] {.since: (1, 3).} =
## Creates a new deque that contains the elements of `x` (in the same order).
##
## **See also:**
## * `initDeque proc <#initDeque,int>`_
runnableExamples:
let a = toDeque([7, 8, 9])
assert len(a) == 3
assert $a == "[7, 8, 9]"

result.initImpl(x.len)
for item in items(x):
result.addLast(item)

proc len*[T](deq: Deque[T]): int {.inline.} =
## Returns the number of elements of `deq`.
result = deq.count
var x = @[10, 20, 30].toDeque
assert x.len == 3
assert x[0] == 10
x.addFirst 0
x.addLast 40
assert $x == "[0, 10, 20, 30, 40]"
result.head = 0
result.count = x.len
result.tail = x.len
if x.len.isPowerOfTwo:
result.data.add x
else:
let n = x.len.nextPowerOfTwo
result.data = newSeqOfCap[T](n)
result.data.add x
result.data.setLen n
result.mask = result.data.len - 1

template emptyCheck(deq) =
# Bounds check for the regular deque access.
Expand Down Expand Up @@ -178,7 +196,7 @@ proc `[]`*[T](deq: var Deque[T], i: BackwardsIndex): var T {.inline.} =
assert a[^1] == 51

xBoundsCheck(deq, deq.len - int(i))
return deq[deq.len - int(i)]
return deq.data[(deq.head + (deq.len - int(i))) and deq.mask]

proc `[]=`*[T](deq: var Deque[T], i: BackwardsIndex, x: T) {.inline.} =
## Sets the backwards indexed `i`-th element of `deq` to `x`.
Expand All @@ -192,7 +210,7 @@ proc `[]=`*[T](deq: var Deque[T], i: BackwardsIndex, x: T) {.inline.} =

checkIfInitialized(deq)
xBoundsCheck(deq, deq.len - int(i))
deq[deq.len - int(i)] = x
deq.data[(deq.head + (deq.len - int(i))) and deq.mask] = x

iterator items*[T](deq: Deque[T]): T =
## Yields every element of `deq`.
Expand Down Expand Up @@ -276,7 +294,7 @@ proc addFirst*[T](deq: var Deque[T], item: T) =
## **See also:**
## * `addLast proc <#addLast,Deque[T],T>`_
runnableExamples:
var a = initDeque[int]()
var a: Deque[int]
for i in 1 .. 5:
a.addFirst(10 * i)
assert $a == "[50, 40, 30, 20, 10]"
Expand All @@ -292,7 +310,7 @@ proc addLast*[T](deq: var Deque[T], item: T) =
## **See also:**
## * `addFirst proc <#addFirst,Deque[T],T>`_
runnableExamples:
var a = initDeque[int]()
var a: Deque[int]
for i in 1 .. 5:
a.addLast(10 * i)
assert $a == "[10, 20, 30, 40, 50]"
Expand Down Expand Up @@ -421,7 +439,7 @@ proc shrink*[T](deq: var Deque[T], fromFirst = 0, fromLast = 0) =
## `fromLast` elements from the back.
##
## If the supplied number of elements exceeds the total number of elements
## in the deque, the deque will remain empty.
## in the deque, the deque will be emptied entirely.
##
## **See also:**
## * `clear proc <#clear,Deque[T]>`_
Expand Down Expand Up @@ -458,3 +476,20 @@ proc `$`*[T](deq: Deque[T]): string =
if result.len > 1: result.add(", ")
result.addQuoted(x)
result.add("]")

proc hash*[A](d: Deque[A]): Hash =
## Hashing of Deque.
runnableExamples:
var
x: Deque[int]
y: Deque[int]

for i in 1..5:
x.addLast(i*10)
for i in -5..10:
y.addLast(i*10)
y.shrink(fromFirst = 6, fromLast = 5)
assert hash(x) == hash(y)
for h in d:
result = result !& hash(h)
result = !$result
6 changes: 3 additions & 3 deletions lib/pure/collections/setimpl.nim
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ template initImpl(s: typed, size: int) =

template rawInsertImpl() {.dirty.} =
if data.len == 0:
initImpl(s, defaultInitialSize)
initImpl(s, nimSetDefaultInitialCapacity)
data[h].key = key
data[h].hcode = hc

Expand All @@ -44,7 +44,7 @@ proc enlarge[A](s: var HashSet[A]) =

template inclImpl() {.dirty.} =
if s.data.len == 0:
initImpl(s, defaultInitialSize)
initImpl(s, nimSetDefaultInitialCapacity)
var hc: Hash
var index = rawGet(s, key, hc)
if index < 0:
Expand All @@ -56,7 +56,7 @@ template inclImpl() {.dirty.} =

template containsOrInclImpl() {.dirty.} =
if s.data.len == 0:
initImpl(s, defaultInitialSize)
initImpl(s, nimSetDefaultInitialCapacity)
var hc: Hash
var index = rawGet(s, key, hc)
if index >= 0:
Expand Down
17 changes: 8 additions & 9 deletions lib/pure/collections/sets.nim
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ type
## Type union representing `HashSet` or `OrderedSet`.

const
defaultInitialSize* = 64
nimSetDefaultInitialCapacity* {.intdefine.} = 64

include setimpl

Expand All @@ -90,7 +90,7 @@ include setimpl
# ---------------------------------------------------------------------


proc init*[A](s: var HashSet[A], initialSize = defaultInitialSize) =
proc init*[A](s: var HashSet[A], initialSize = nimSetDefaultInitialCapacity) =
## Initializes a hash set.
##
## Starting from Nim v0.20, sets are initialized by default and it is
Expand All @@ -109,8 +109,8 @@ proc init*[A](s: var HashSet[A], initialSize = defaultInitialSize) =

initImpl(s, initialSize)

proc initHashSet*[A](initialSize = defaultInitialSize): HashSet[A] =
## Wrapper around `init proc <#init,HashSet[A]>`_ for initialization of
proc initHashSet*[A](initialSize = nimSetDefaultInitialCapacity): HashSet[A] =
## Wrapper around `init proc <#init,HashSet[A],int>`_ for initialization of
## hash sets.
##
## Returns an empty hash set you can assign directly in `var` blocks in a
Expand Down Expand Up @@ -591,8 +591,7 @@ proc `$`*[A](s: HashSet[A]): string =
## # --> {no, esc'aping, is " provided}
dollarImpl()


proc initSet*[A](initialSize = defaultInitialSize): HashSet[A] {.deprecated:
proc initSet*[A](initialSize = nimSetDefaultInitialCapacity): HashSet[A] {.deprecated:
"Deprecated since v0.20, use 'initHashSet'".} = initHashSet[A](initialSize)

proc toSet*[A](keys: openArray[A]): HashSet[A] {.deprecated:
Expand Down Expand Up @@ -629,7 +628,7 @@ template forAllOrderedPairs(yieldStmt: untyped) {.dirty.} =
h = nxt


proc init*[A](s: var OrderedSet[A], initialSize = defaultInitialSize) =
proc init*[A](s: var OrderedSet[A], initialSize = nimSetDefaultInitialCapacity) =
## Initializes an ordered hash set.
##
## Starting from Nim v0.20, sets are initialized by default and it is
Expand All @@ -648,8 +647,8 @@ proc init*[A](s: var OrderedSet[A], initialSize = defaultInitialSize) =

initImpl(s, initialSize)

proc initOrderedSet*[A](initialSize = defaultInitialSize): OrderedSet[A] =
## Wrapper around `init proc <#init,OrderedSet[A]>`_ for initialization of
proc initOrderedSet*[A](initialSize = nimSetDefaultInitialCapacity): OrderedSet[A] =
## Wrapper around `init proc <#init,OrderedSet[A],int>`_ for initialization of
## ordered hash sets.
##
## Returns an empty ordered hash set you can assign directly in `var` blocks
Expand Down
6 changes: 3 additions & 3 deletions lib/pure/collections/tableimpl.nim
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ proc rawInsert[X, A, B](t: var X, data: var KeyValuePairSeq[A, B],
rawInsertImpl()

template checkIfInitialized() =
when compiles(defaultInitialSize):
when declared(nimTableDefaultInitialCapacity):
if t.dataLen == 0:
initImpl(t, defaultInitialSize)
initImpl(t, nimTableDefaultInitialCapacity)

template addImpl(enlarge) {.dirty.} =
checkIfInitialized()
Expand Down Expand Up @@ -171,7 +171,7 @@ template initImpl(result: typed, size: int) =
result.last = -1

template insertImpl() = # for CountTable
if t.dataLen == 0: initImpl(t, defaultInitialSize)
if t.dataLen == 0: initImpl(t, nimTableDefaultInitialCapacity)
if mustRehash(t): enlarge(t)
ctRawInsert(t, t.data, key, val)
inc(t.counter)
Expand Down
14 changes: 7 additions & 7 deletions lib/pure/collections/tables.nim
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ type
## <#newTable>`_.

const
defaultInitialSize* = 32
nimTableDefaultInitialCapacity* {.intdefine.} = 64

# ------------------------------ helpers ---------------------------------

Expand Down Expand Up @@ -262,7 +262,7 @@ proc enlarge[A, B](t: var Table[A, B]) =
# ------------------------------ Table ------------------------------
# -------------------------------------------------------------------

proc initTable*[A, B](initialSize = defaultInitialSize): Table[A, B] =
proc initTable*[A, B](initialSize = nimTableDefaultInitialCapacity): Table[A, B] =
## Creates a new hash table that is empty.
##
## Starting from Nim v0.20, tables are initialized by default and it is
Expand Down Expand Up @@ -795,7 +795,7 @@ iterator allValues*[A, B](t: Table[A, B]; key: A): B {.deprecated:
# -------------------------------------------------------------------


proc newTable*[A, B](initialSize = defaultInitialSize): <//>TableRef[A, B] =
proc newTable*[A, B](initialSize = nimTableDefaultInitialCapacity): <//>TableRef[A, B] =
## Creates a new ref hash table that is empty.
##
## See also:
Expand Down Expand Up @@ -1286,7 +1286,7 @@ template forAllOrderedPairs(yieldStmt: untyped) {.dirty.} =

# ----------------------------------------------------------------------

proc initOrderedTable*[A, B](initialSize = defaultInitialSize): OrderedTable[A, B] =
proc initOrderedTable*[A, B](initialSize = nimTableDefaultInitialCapacity): OrderedTable[A, B] =
## Creates a new ordered hash table that is empty.
##
## Starting from Nim v0.20, tables are initialized by default and it is
Expand Down Expand Up @@ -1790,7 +1790,7 @@ iterator mvalues*[A, B](t: var OrderedTable[A, B]): var B =
# --------------------------- OrderedTableRef -------------------------------
# ---------------------------------------------------------------------------

proc newOrderedTable*[A, B](initialSize = defaultInitialSize): <//>OrderedTableRef[A, B] =
proc newOrderedTable*[A, B](initialSize = nimTableDefaultInitialCapacity): <//>OrderedTableRef[A, B] =
## Creates a new ordered ref hash table that is empty.
##
## See also:
Expand Down Expand Up @@ -2252,7 +2252,7 @@ proc inc*[A](t: var CountTable[A], key: A, val = 1)

# ----------------------------------------------------------------------

proc initCountTable*[A](initialSize = defaultInitialSize): CountTable[A] =
proc initCountTable*[A](initialSize = nimTableDefaultInitialCapacity): CountTable[A] =
## Creates a new count table that is empty.
##
## Starting from Nim v0.20, tables are initialized by default and it is
Expand Down Expand Up @@ -2610,7 +2610,7 @@ iterator mvalues*[A](t: var CountTable[A]): var int =

proc inc*[A](t: CountTableRef[A], key: A, val = 1)

proc newCountTable*[A](initialSize = defaultInitialSize): <//>CountTableRef[A] =
proc newCountTable*[A](initialSize = nimTableDefaultInitialCapacity): <//>CountTableRef[A] =
## Creates a new ref count table that is empty.
##
## See also:
Expand Down
13 changes: 13 additions & 0 deletions tests/collections/tcollections.nim
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ block tapply:
x.applyIt(it+5000)
doAssert x == @[5111, 5112, 5113]

block tdeques:
proc index(self: Deque[int], idx: Natural): int =
self[idx]

proc main =
var testDeque: Deque[int]
testDeque.addFirst(1)
assert testDeque.index(0) == 1
assert testDeque[^1] == 1

main()


block tmapit:
var x = @[1, 2, 3]
# This mapIt call will run with preallocation because ``len`` is available.
Expand Down
Loading