From 78f01cfcb3e8081e4cdc6b99e0a2c07f04fb2ba6 Mon Sep 17 00:00:00 2001 From: Eduardo Bart Date: Tue, 17 Nov 2020 16:28:33 -0300 Subject: [PATCH] Add double linked list module in the standard library --- examples/linkedlist.nelua | 250 --------------------------------- lib/iterators.nelua | 86 ++++++++---- lib/list.nelua | 232 ++++++++++++++++++++++++++++++ lib/sequence.nelua | 1 + lib/span.nelua | 1 + lib/vector.nelua | 7 +- nelua/types.lua | 3 + rockspecs/nelua-dev-1.rockspec | 1 + tests/all_test.nelua | 4 + tests/list_test.nelua | 128 +++++++++++++++++ 10 files changed, 435 insertions(+), 278 deletions(-) delete mode 100644 examples/linkedlist.nelua create mode 100644 lib/list.nelua create mode 100644 tests/list_test.nelua diff --git a/examples/linkedlist.nelua b/examples/linkedlist.nelua deleted file mode 100644 index 0c796210..00000000 --- a/examples/linkedlist.nelua +++ /dev/null @@ -1,250 +0,0 @@ -require 'allocators.general' - -local allocator: auto = general_allocator - -## local make_list = generalize(function(T) - local T = @#[T]# - local listnodeT = @record { - prev: *listnodeT, - next: *listnodeT, - value: T - } - - local function new_node(prev: *listnodeT, next: *listnodeT, value: T): *listnodeT - local node = allocator:new(@listnodeT) - node.prev = prev - node.next = next - node.value = value - return node - end - - local listT = @record{ - head: *listnodeT, - tail: *listnodeT - } - - function listT:prepend(value: T) - local node = new_node(nilptr, self.head, value) - if likely(self.head) then - assert(not self.head.prev) - self.head.prev = node - end - if unlikely(not self.tail) then - self.tail = node - end - self.head = node - end - - function listT:append(value: T) - local node = new_node(self.tail, nilptr, value) - if likely(self.tail) then - assert(not self.tail.next) - self.tail.next = node - end - if unlikely(not self.head) then - self.head = node - end - self.tail = node - end - - function listT:find(value: T): *listnodeT - local it = self.head - while it do - if it.value == value then - return it - end - it = it.next - end - return nilptr - end - - function listT:contains(value: T): boolean - return self:find(value) ~= nilptr - end - - function listT:erase(node: *listnodeT): *listnodeT - if node == self.head then - self.head = node.next - end - if node == self.tail then - self.tail = node.prev - end - if node.prev then - node.prev.next = node.next - end - if node.next then - node.next.prev = node.prev - end - local next = node.next - allocator:delete(node) - return next - end - - function listT:remove_first() - assert(self.head) - local node = self.head - self.head = node.next - if likely(self.head) then - self.head.prev = nilptr - end - if unlikely(self.tail == node) then - self.tail = nilptr - end - allocator:delete(node) - end - - function listT:remove_last() - assert(self.tail) - local node = self.tail - self.tail = node.prev - if likely(self.tail) then - self.tail.next = nilptr - end - if unlikely(self.head == node) then - self.head = nilptr - end - allocator:delete(node) - end - - function listT:clear() - local it = self.head - while it do - local next = it.next - allocator:delete(it) - it = next - end - self.head = nilptr - self.tail = nilptr - end - - function listT:__len(): isize - local count: isize = 0 - local it = self.head - while it do - count = count + 1 - it = it.next - end - return count - end - - function listT:__next(node: *listnodeT): (boolean, *listnodeT, T) - local nextnode: *listnodeT - if unlikely(node == nilptr) then - nextnode = self.head - else - nextnode = node.next - end - if unlikely(nextnode == nilptr) then - return false, nilptr, T() - end - return true, nextnode, nextnode.value - end - - function listT:__pairs() - return listT.__next, self, nilptr - end - - function listT:empty(): boolean - return self.head == nilptr - end - - -- TODO: insert() -- (like lua, supports append and prepend) - -- TODO: remove() -- (like lua, supports first and last) - - ## return listT -## end) - -local list = #[make_list]# - ------------------------------------------------------------------------ --- tests -do - local l: list(integer) - assert(#l == 0 and l:empty()) - l:append(1) assert(l.head.value == 1) assert(l.tail.value == 1) - assert(#l == 1 and not l:empty()) - l:append(2) assert(l.head.value == 1) assert(l.tail.value == 2) - l:prepend(3) assert(l.head.value == 3) assert(l.tail.value == 2) - assert(#l == 3) - assert(l:contains(1) and l:contains(2) and l:contains(3)) - assert(not l:contains(0)) - l:clear() - assert(not l.head and not l.tail) - assert(l:empty()) -end - -do -- erase - local l: list(integer) - l:append(1) - assert(l:erase(l.tail) == nilptr) - assert(l:empty()) - - l:append(1) - assert(l:erase(l.head) == nilptr) - assert(l:empty()) - - l:append(1) l:append(2) - assert(l:erase(l.head) == l.tail) - l:clear() - - l:append(1) l:append(2) - assert(l:erase(l.tail) == nilptr) - l:clear() - - l:append(1) l:append(2) l:append(3) - local it = l.head.next - assert(it.value == 2) - assert(l:erase(it) == l.tail) - assert(l:erase(l.head) == l.tail) - assert(l:erase(l.tail) == nilptr) - assert(l:empty()) -end - -do -- find - local l: list(integer) - l:append(1) l:append(2) l:append(3) - assert(l:find(1) == l.head) - assert(l:find(2) == l.head.next and l:find(2) == l.tail.prev) - assert(l:find(3) == l.tail) - l:clear() - assert(l:empty()) -end - -do -- remove_last - local l: list(integer) - l:append(1) l:append(2) l:append(3) - l:remove_last() - assert(l.tail.value == 2) - assert(l.head.value == 1) - assert(l.tail.next == nilptr and l.head.prev == nilptr) - l:remove_last() - assert(l.tail.value == 1) - assert(l.head.value == 1) - assert(l.tail.next == nilptr and l.head.prev == nilptr) - l:remove_last() - assert(l:empty()) -end - -do -- remove_first - local l: list(integer) - l:append(1) l:append(2) l:append(3) - l:remove_first() - assert(l.tail.value == 3) - assert(l.head.value == 2) - assert(l.tail.next == nilptr and l.head.prev == nilptr) - l:remove_first() - assert(l.tail.value == 3) - assert(l.head.value == 3) - assert(l.tail.next == nilptr and l.head.prev == nilptr) - l:remove_first() - assert(l:empty()) -end - -do -- iterate - local l: list(integer) - l:append(1) l:append(2) l:append(3) - for _,v in l:__pairs() do - print(v) - end - l:clear() -end diff --git a/lib/iterators.nelua b/lib/iterators.nelua index 2e5273b7..4290f7de 100644 --- a/lib/iterators.nelua +++ b/lib/iterators.nelua @@ -1,25 +1,39 @@ -- Include this file to get the globals "pairs", "ipairs" and "next", -- to be used with when iterating with "for in". --- Concept used to pass containers by reference. -local list_reference_concept = #[concept(function(x) +##[[ +local function container_type_by_reference(xtype) local reftype local containertype - if x.type.is_pointer then - reftype = x.type + if xtype.is_pointer then + reftype = xtype containertype = reftype.subtype - elseif x.type.is_span or x.type.is_sequence then - reftype = x.type + elseif xtype.is_span or xtype.is_sequence then + reftype = xtype containertype = reftype else - containertype = x.type + containertype = xtype reftype = types.PointerType(containertype) end + return containertype, reftype +end +]] +-- Concept used to pass contiguous containers by reference. +local contiguous_reference_concept = #[concept(function(x) + local containertype, reftype = container_type_by_reference(x.type) if containertype.is_contiguous then return reftype end end)]# +-- Concept used to pass containers by reference. +local container_reference_concept = #[concept(function(x) + local containertype, reftype = container_type_by_reference(x.type) + if containertype.is_container then + return reftype + end +end)]# + -- Macro that implements the next iterator for lists. ## local function impl_ipairs_next(listtype) index = index + 1 @@ -38,36 +52,60 @@ end)]# return true, index, &list[index] ## end --- Use with "for in" to iterate lists. -global function ipairs(list: list_reference_concept) - ## local listvaltype = list.type:implict_deref_type() +-- Use with "for in" to iterate contiguous containers. +global function ipairs(list: contiguous_reference_concept) + ## local listtype = list.type:implict_deref_type() local function ipairs_next(list: #[list.type]#, index: integer) - ## impl_ipairs_next(listvaltype) + ## impl_ipairs_next(listtype) end - return ipairs_next, list, #[listvaltype.is_oneindexing and 0 or -1]# + return ipairs_next, list, #[listtype.is_oneindexing and 0 or -1]# end -- Like `ipairs` but yields reference to elements so that you can modify. -global function mipairs(list: list_reference_concept) - ## local listvaltype = list.type:implict_deref_type() +global function mipairs(list: contiguous_reference_concept) + ## local listtype = list.type:implict_deref_type() local function mipairs_next(list: #[list.type]#, index: integer) - ## impl_mipairs_next(listvaltype) + ## impl_mipairs_next(listtype) end - return mipairs_next, list, #[listvaltype.is_oneindexing and 0 or -1]# + return mipairs_next, list, #[listtype.is_oneindexing and 0 or -1]# end -- Get the next element from a container. -global function next(list: list_reference_concept, - index: facultative(integer)) - ## impl_ipairs_next(list.type:implict_deref_type()) +global function next(list: container_reference_concept, index: auto) + ## local listtype = list.type:implict_deref_type() + ## if listtype.metafields.__next then + return list:__next(index) + ## else + ## impl_ipairs_next(list.type:implict_deref_type()) + ## end end -- Like `next` but returns reference to elements so that you can modify. -global function mnext(list: list_reference_concept, - index: facultative(integer)) +global function mnext(list: contiguous_reference_concept, index: auto) + ## local listtype = list.type:implict_deref_type() + ## if listtype.metafields.__next then + return list:__mnext(index) + ## else ## impl_mipairs_next(list.type:implict_deref_type()) + ## end end --- at the moment pairs only works like ipairs -global pairs: auto = ipairs -global mpairs: auto = mipairs +-- Use with "for in" to iterate containers. +global function pairs(list: container_reference_concept) + ## local listtype = list.type:implict_deref_type() + ## if listtype.metafields.__pairs then + return list:__pairs() + ## else + return ipairs(list) + ## end +end + +-- Like `pairs` but yields reference to elements so that you can modify. +global function mpairs(list: container_reference_concept) + ## local listtype = list.type:implict_deref_type() + ## if listtype.metafields.__mpairs then + return list:__mpairs() + ## else + return mipairs(list) + ## end +end diff --git a/lib/list.nelua b/lib/list.nelua new file mode 100644 index 00000000..4da99734 --- /dev/null +++ b/lib/list.nelua @@ -0,0 +1,232 @@ +-- List type +-- +-- This type implements a double linked list. +-- Supports constant time insertion and removal of elements from anywhere in the container. +-- Fast random access is not supported. +-- +-- By default it uses the garbage collector unless explicitly told not to do so, +-- thus by default there is no need to manually destroy the list. + +## local make_generic_list_node = generalize(function(T) + local T: type = @#[T]# + local listnodeT: type = @record { + prev: *listnodeT, + next: *listnodeT, + value: T + } + ## return listnodeT +## end) + +global list_node: type = #[make_generic_list_node]# + +## local make_generic_list = generalize(function(T, Allocator) + ## static_assert(traits.is_type(T), "invalid type '%s'", T) + ## if not Allocator then + require 'allocators.default' + ## Allocator = DefaultAllocator + ## end + + local Allocator: type = #[Allocator]# + local T: type = @#[T]# + local listnodeT: type = @list_node(T) + + local listT = @record{ + head: *listnodeT, -- list begin + tail: *listnodeT, -- list end + allocator: Allocator + } + + ##[[ + local listT = listT.value + listT.is_list = true + listT.is_container = true + listT.subtype = T + ]] + + function listT:_new_node(prev: *listnodeT, next: *listnodeT, value: T): *listnodeT + local node: *listnodeT = self.allocator:new(@listnodeT) + node.prev = prev + node.next = next + node.value = value + return node + end + + -- Create a list using a custom allocator instance. + -- This is only to be used when not using the default allocator. + function listT.make(allocator: Allocator): listT + local l: listT + l.allocator = allocator + return l + end + + -- Remove all elements from the list. + -- O(n) complexity. + function listT:clear() + local it: *listnodeT = self.head + while it do + local next: *listnodeT = it.next + self.allocator:delete(it) + it = next + end + self.head = nilptr + self.tail = nilptr + end + + -- Resets the list to zeroed state, freeing all used resources. + -- This is more useful to free resources when not using the garbage collector. + function listT:destroy() + self:clear() + $self = (@listT)() + end + + -- Insert an element at beginning of the list. + -- O(1) complexity. + function listT:prepend(value: T) + local node: *listnodeT = self:_new_node(nilptr, self.head, value) + if likely(self.head) then + self.head.prev = node + end + if unlikely(not self.tail) then + self.tail = node + end + self.head = node + end + + -- Add an element at the end of the list. + -- O(1) complexity. + function listT:append(value: T) + local node: *listnodeT = self:_new_node(self.tail, nilptr, value) + if likely(self.tail) then + self.tail.next = node + end + if unlikely(not self.head) then + self.head = node + end + self.tail = node + end + + -- Find an element in the list, returning it's node reference when found. + -- O(n) complexity. + function listT:find(value: T): *listnodeT + local it: *listnodeT = self.head + while it do + if it.value == value then + return it + end + it = it.next + end + return nilptr + end + + -- Erase an element where from the list. + -- O(1) complexity. + function listT:erase(node: *listnodeT): *listnodeT + check(node ~= nilptr, 'listT.erase: attempt to erase a nilptr node') + if node == self.head then + self.head = node.next + end + if node == self.tail then + self.tail = node.prev + end + if node.prev then + node.prev.next = node.next + end + if node.next then + node.next.prev = node.prev + end + local next: *listnodeT = node.next + self.allocator:delete(node) + return next + end + + -- Remove first element from the list. + -- O(1) complexity. + function listT:remove_first() + check(self.head ~= nilptr, 'list.remove_first: list is empty') + local node: *listnodeT = self.head + self.head = node.next + if likely(self.head) then + self.head.prev = nilptr + end + if unlikely(self.tail == node) then + self.tail = nilptr + end + self.allocator:delete(node) + end + + -- Remove last element from the list. + -- O(1) complexity. + function listT:remove_last() + check(self.tail ~= nilptr, 'list.remove_first: list is empty') + local node: *listnodeT = self.tail + self.tail = node.prev + if likely(self.tail) then + self.tail.next = nilptr + end + if unlikely(self.head == node) then + self.head = nilptr + end + self.allocator:delete(node) + end + + -- Test whether the list is empty. + function listT:empty(): boolean + return self.head == nilptr + end + + -- Returns the number of elements in the list. + -- O(n) complexity. + function listT:__len(): isize + local count: isize = 0 + local it: *listnodeT = self.head + while it do + count = count + 1 + it = it.next + end + return count + end + + -- Returns the next node of the list and its element. + -- Used with pairs() iterator. + function listT:__next(node: *listnodeT): (boolean, *listnodeT, T) + local nextnode: *listnodeT + if unlikely(node == nilptr) then + nextnode = self.head + else + nextnode = node.next + end + if unlikely(nextnode == nilptr) then + return false, nilptr, T() + end + return true, nextnode, nextnode.value + end + + -- Returns the next node of the list and its element by reference. + -- Used with pairs() iterator. + function listT:__mnext(node: *listnodeT): (boolean, *listnodeT, *T) + local nextnode: *listnodeT + if unlikely(node == nilptr) then + nextnode = self.head + else + nextnode = node.next + end + if unlikely(nextnode == nilptr) then + return false, nilptr, nilptr + end + return true, nextnode, &nextnode.value + end + + -- Allow using pairs() iterator with the list. + function listT:__pairs() + return listT.__next, self, nilptr + end + + -- Allow using pairs() iterator with the list. + function listT:__mpairs() + return listT.__mnext, self, nilptr + end + + ## return listT +## end) + +global list: type = #[make_generic_list]# diff --git a/lib/sequence.nelua b/lib/sequence.nelua index 8318bd0f..98c2408f 100644 --- a/lib/sequence.nelua +++ b/lib/sequence.nelua @@ -33,6 +33,7 @@ require 'iterators' ##[[ local sequenceT = sequenceT.value sequenceT.is_contiguous = true + sequenceT.is_container = true sequenceT.is_sequence = true sequenceT.is_oneindexing = true -- used in 'ipairs' sequenceT.subtype = T diff --git a/lib/span.nelua b/lib/span.nelua index b0aa2248..e78a7930 100644 --- a/lib/span.nelua +++ b/lib/span.nelua @@ -20,6 +20,7 @@ ##[[ spanT.value.is_contiguous = true + spanT.value.is_container = true spanT.value.is_span = true spanT.value.subtype = T ]] diff --git a/lib/vector.nelua b/lib/vector.nelua index c159e8bc..2789b61b 100644 --- a/lib/vector.nelua +++ b/lib/vector.nelua @@ -5,7 +5,7 @@ -- At the moment this should not be passed as value, only as reference. -- -- By default it uses the garbage collector unless explicitly told not to do so, --- thus by default there is no need to manually reset the vector. +-- thus by default there is no need to manually destroy the vector. require 'memory' require 'iterators' @@ -28,6 +28,7 @@ require 'iterators' ##[[ local vectorT = vectorT.value vectorT.is_contiguous = true + vectorT.is_container = true vectorT.is_vector = true vectorT.subtype = T vectorT.choose_braces_type = function(nodes) return types.ArrayType(T, #nodes) end @@ -50,9 +51,7 @@ require 'iterators' -- Removes all elements from the vector. function vectorT:clear() - for i:usize=0,