diff --git a/core/container/queue/queue.odin b/core/container/queue/queue.odin index d1040a7c9..7f6f55826 100644 --- a/core/container/queue/queue.odin +++ b/core/container/queue/queue.odin @@ -4,7 +4,13 @@ import "base:builtin" import "base:runtime" _ :: runtime -// Dynamically resizable double-ended queue/ring-buffer +/* +`Queue` is a dynamically resizable double-ended queue/ring-buffer. + +Being double-ended means that either end may be pushed onto or popped from +across the same block of memory, in any order, thus providing both stack and +queue-like behaviors in the same data structure. +*/ Queue :: struct($T: typeid) { data: [dynamic]T, len: uint, @@ -13,18 +19,31 @@ Queue :: struct($T: typeid) { DEFAULT_CAPACITY :: 16 -// Procedure to initialize a queue +/* +Initialize a `Queue` with a starting `capacity` and an `allocator`. +*/ init :: proc(q: ^$Q/Queue($T), capacity := DEFAULT_CAPACITY, allocator := context.allocator) -> runtime.Allocator_Error { - if q.data.allocator.procedure == nil { - q.data.allocator = allocator - } clear(q) + q.data = transmute([dynamic]T)runtime.Raw_Dynamic_Array{ + data = nil, + len = 0, + cap = 0, + allocator = allocator, + } return reserve(q, capacity) } -// Procedure to initialize a queue from a fixed backing slice. -// The contents of the `backing` will be overwritten as items are pushed onto the `Queue`. -// Any previous contents are not available. +/* +Initialize a `Queue` from a fixed `backing` slice into which modifications are +made directly. + +The contents of the `backing` will be overwritten as items are pushed onto the +`Queue`. Any previous contents will not be available through the API but are +not explicitly zeroed either. + +Note that procedures which need space to work (`push_back`, ...) will fail if +the backing slice runs out of space. +*/ init_from_slice :: proc(q: ^$Q/Queue($T), backing: []T) -> bool { clear(q) q.data = transmute([dynamic]T)runtime.Raw_Dynamic_Array{ @@ -36,8 +55,14 @@ init_from_slice :: proc(q: ^$Q/Queue($T), backing: []T) -> bool { return true } -// Procedure to initialize a queue from a fixed backing slice. -// Existing contents are preserved and available on the queue. +/* +Initialize a `Queue` from a fixed `backing` slice into which modifications are +made directly. + +The contents of the queue will start out with all of the elements in `backing`, +effectively creating a full queue from the slice. As such, no procedures will +be able to add more elements to the queue until some are taken off. +*/ init_with_contents :: proc(q: ^$Q/Queue($T), backing: []T) -> bool { clear(q) q.data = transmute([dynamic]T)runtime.Raw_Dynamic_Array{ @@ -50,84 +75,200 @@ init_with_contents :: proc(q: ^$Q/Queue($T), backing: []T) -> bool { return true } -// Procedure to destroy a queue +/* +Delete memory that has been dynamically allocated from a `Queue` that was setup with `init`. + +Note that this procedure should not be used on queues setup with +`init_from_slice` or `init_with_contents`, as neither of those procedures keep +track of the allocator state of the underlying `backing` slice. +*/ destroy :: proc(q: ^$Q/Queue($T)) { delete(q.data) } -// The length of the queue +/* +Return the length of the queue. +*/ len :: proc(q: $Q/Queue($T)) -> int { return int(q.len) } -// The current capacity of the queue +/* +Return the capacity of the queue. +*/ cap :: proc(q: $Q/Queue($T)) -> int { return builtin.len(q.data) } -// Remaining space in the queue (cap-len) +/* +Return the remaining space in the queue. + +This will be `cap() - len()`. +*/ space :: proc(q: $Q/Queue($T)) -> int { return builtin.len(q.data) - int(q.len) } -// Reserve enough space for at least the specified capacity +/* +Reserve enough space in the queue for at least the specified capacity. + +This may return an error if allocation failed. +*/ reserve :: proc(q: ^$Q/Queue($T), capacity: int) -> runtime.Allocator_Error { if capacity > space(q^) { - return _grow(q, uint(capacity)) + return _grow(q, uint(capacity)) } return nil } +/* +Shrink a queue's dynamically allocated array. +This has no effect if the queue was initialized with a backing slice. +*/ +shrink :: proc(q: ^$Q/Queue($T), temp_allocator := context.temp_allocator, loc := #caller_location) { + if q.data.allocator.procedure == runtime.nil_allocator_proc { + return + } + + if q.len > 0 && q.offset > 0 { + // Make the array contiguous again. + buffer := make([]T, q.len, temp_allocator) + defer delete(buffer, temp_allocator) + + right := uint(builtin.len(q.data)) - q.offset + copy(buffer[:], q.data[q.offset:]) + copy(buffer[right:], q.data[:q.offset]) + + copy(q.data[:], buffer[:]) + + q.offset = 0 + } + + builtin.shrink(&q.data, q.len, loc) +} + +/* +Get the element at index `i`. + +This will raise a bounds checking error if `i` is an invalid index. +*/ get :: proc(q: ^$Q/Queue($T), #any_int i: int, loc := #caller_location) -> T { - runtime.bounds_check_error_loc(loc, i, builtin.len(q.data)) + runtime.bounds_check_error_loc(loc, i, int(q.len)) idx := (uint(i)+q.offset)%builtin.len(q.data) return q.data[idx] } -front :: proc(q: ^$Q/Queue($T)) -> T { - return q.data[q.offset] -} -front_ptr :: proc(q: ^$Q/Queue($T)) -> ^T { - return &q.data[q.offset] -} +/* +Get a pointer to the element at index `i`. -back :: proc(q: ^$Q/Queue($T)) -> T { - idx := (q.offset+uint(q.len - 1))%builtin.len(q.data) - return q.data[idx] -} -back_ptr :: proc(q: ^$Q/Queue($T)) -> ^T { - idx := (q.offset+uint(q.len - 1))%builtin.len(q.data) +This will raise a bounds checking error if `i` is an invalid index. +*/ +get_ptr :: proc(q: ^$Q/Queue($T), #any_int i: int, loc := #caller_location) -> ^T { + runtime.bounds_check_error_loc(loc, i, int(q.len)) + + idx := (uint(i)+q.offset)%builtin.len(q.data) return &q.data[idx] } +/* +Set the element at index `i` to `val`. + +This will raise a bounds checking error if `i` is an invalid index. +*/ set :: proc(q: ^$Q/Queue($T), #any_int i: int, val: T, loc := #caller_location) { - runtime.bounds_check_error_loc(loc, i, builtin.len(q.data)) - + runtime.bounds_check_error_loc(loc, i, int(q.len)) + idx := (uint(i)+q.offset)%builtin.len(q.data) q.data[idx] = val } -get_ptr :: proc(q: ^$Q/Queue($T), #any_int i: int, loc := #caller_location) -> ^T { - runtime.bounds_check_error_loc(loc, i, builtin.len(q.data)) - - idx := (uint(i)+q.offset)%builtin.len(q.data) + +/* +Get the element at the front of the queue. + +This will raise a bounds checking error if the queue is empty. +*/ +front :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> T { + when !ODIN_NO_BOUNDS_CHECK { + ensure(q.len > 0, "Queue is empty.", loc) + } + return q.data[q.offset] +} + +/* +Get a pointer to the element at the front of the queue. + +This will raise a bounds checking error if the queue is empty. +*/ +front_ptr :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> ^T { + when !ODIN_NO_BOUNDS_CHECK { + ensure(q.len > 0, "Queue is empty.", loc) + } + return &q.data[q.offset] +} + +/* +Get the element at the back of the queue. + +This will raise a bounds checking error if the queue is empty. +*/ +back :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> T { + when !ODIN_NO_BOUNDS_CHECK { + ensure(q.len > 0, "Queue is empty.", loc) + } + idx := (q.offset+uint(q.len - 1))%builtin.len(q.data) + return q.data[idx] +} + +/* +Get a pointer to the element at the back of the queue. + +This will raise a bounds checking error if the queue is empty. +*/ +back_ptr :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> ^T { + when !ODIN_NO_BOUNDS_CHECK { + ensure(q.len > 0, "Queue is empty.", loc) + } + idx := (q.offset+uint(q.len - 1))%builtin.len(q.data) return &q.data[idx] } + +@(deprecated="Use `front_ptr` instead") peek_front :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> ^T { - runtime.bounds_check_error_loc(loc, 0, builtin.len(q.data)) - idx := q.offset%builtin.len(q.data) - return &q.data[idx] + return front_ptr(q, loc) } +@(deprecated="Use `back_ptr` instead") peek_back :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> ^T { - runtime.bounds_check_error_loc(loc, int(q.len - 1), builtin.len(q.data)) - idx := (uint(q.len - 1)+q.offset)%builtin.len(q.data) - return &q.data[idx] + return back_ptr(q, loc) } -// Push an element to the back of the queue +/* +Push an element to the back of the queue. + +If there is no more space left and allocation fails to get more, this will +return false with an `Allocator_Error`. + +Example: + + import "base:runtime" + import "core:container/queue" + + // This demonstrates typical queue behavior (First-In First-Out). + main :: proc() { + q: queue.Queue(int) + queue.init(&q) + queue.push_back(&q, 1) + queue.push_back(&q, 2) + queue.push_back(&q, 3) + // q.data is now [1, 2, 3, ...] + assert(queue.pop_front(&q) == 1) + assert(queue.pop_front(&q) == 2) + assert(queue.pop_front(&q) == 3) + } +*/ push_back :: proc(q: ^$Q/Queue($T), elem: T) -> (ok: bool, err: runtime.Allocator_Error) { if space(q^) == 0 { _grow(q) or_return @@ -138,27 +279,78 @@ push_back :: proc(q: ^$Q/Queue($T), elem: T) -> (ok: bool, err: runtime.Allocato return true, nil } -// Push an element to the front of the queue +/* +Push an element to the front of the queue. + +If there is no more space left and allocation fails to get more, this will +return false with an `Allocator_Error`. + +Example: + + import "base:runtime" + import "core:container/queue" + + // This demonstrates stack behavior (First-In Last-Out). + main :: proc() { + q: queue.Queue(int) + queue.init(&q) + queue.push_back(&q, 1) + queue.push_back(&q, 2) + queue.push_back(&q, 3) + // q.data is now [1, 2, 3, ...] + assert(queue.pop_back(&q) == 3) + assert(queue.pop_back(&q) == 2) + assert(queue.pop_back(&q) == 1) + } +*/ push_front :: proc(q: ^$Q/Queue($T), elem: T) -> (ok: bool, err: runtime.Allocator_Error) { if space(q^) == 0 { _grow(q) or_return - } + } q.offset = uint(q.offset - 1 + builtin.len(q.data)) % builtin.len(q.data) q.len += 1 q.data[q.offset] = elem return true, nil } +/* +Pop an element from the back of the queue. -// Pop an element from the back of the queue +This will raise a bounds checking error if the queue is empty. + +Example: + + import "base:runtime" + import "core:container/queue" + + // This demonstrates stack behavior (First-In Last-Out) at the far end of the data array. + main :: proc() { + q: queue.Queue(int) + queue.init(&q) + queue.push_front(&q, 1) + queue.push_front(&q, 2) + queue.push_front(&q, 3) + // q.data is now [..., 3, 2, 1] + log.infof("%#v", q) + assert(queue.pop_front(&q) == 3) + assert(queue.pop_front(&q) == 2) + assert(queue.pop_front(&q) == 1) + } +*/ pop_back :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> (elem: T) { - assert(condition=q.len > 0, loc=loc) + when !ODIN_NO_BOUNDS_CHECK { + ensure(q.len > 0, "Queue is empty.", loc) + } q.len -= 1 idx := (q.offset+uint(q.len))%builtin.len(q.data) elem = q.data[idx] return } -// Safely pop an element from the back of the queue + +/* +Pop an element from the back of the queue if one exists and return true. +Otherwise, return a nil element and false. +*/ pop_back_safe :: proc(q: ^$Q/Queue($T)) -> (elem: T, ok: bool) { if q.len > 0 { q.len -= 1 @@ -169,15 +361,25 @@ pop_back_safe :: proc(q: ^$Q/Queue($T)) -> (elem: T, ok: bool) { return } -// Pop an element from the front of the queue +/* +Pop an element from the front of the queue + +This will raise a bounds checking error if the queue is empty. +*/ pop_front :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> (elem: T) { - assert(condition=q.len > 0, loc=loc) + when !ODIN_NO_BOUNDS_CHECK { + ensure(q.len > 0, "Queue is empty.", loc) + } elem = q.data[q.offset] q.offset = (q.offset+1)%builtin.len(q.data) q.len -= 1 return } -// Safely pop an element from the front of the queue + +/* +Pop an element from the front of the queue if one exists and return true. +Otherwise, return a nil element and false. +*/ pop_front_safe :: proc(q: ^$Q/Queue($T)) -> (elem: T, ok: bool) { if q.len > 0 { elem = q.data[q.offset] @@ -188,13 +390,18 @@ pop_front_safe :: proc(q: ^$Q/Queue($T)) -> (elem: T, ok: bool) { return } -// Push multiple elements to the back of the queue +/* +Push many elements at once to the back of the queue. + +If there is not enough space left and allocation fails to get more, this will +return false with an `Allocator_Error`. +*/ push_back_elems :: proc(q: ^$Q/Queue($T), elems: ..T) -> (ok: bool, err: runtime.Allocator_Error) { n := uint(builtin.len(elems)) if space(q^) < int(n) { _grow(q, q.len + n) or_return } - + sz := uint(builtin.len(q.data)) insert_from := (q.offset + q.len) % sz insert_to := n @@ -207,19 +414,31 @@ push_back_elems :: proc(q: ^$Q/Queue($T), elems: ..T) -> (ok: bool, err: runtime return true, nil } -// Consume `n` elements from the front of the queue +/* +Consume `n` elements from the back of the queue. + +This will raise a bounds checking error if the queue does not have enough elements. +*/ consume_front :: proc(q: ^$Q/Queue($T), n: int, loc := #caller_location) { - assert(condition=int(q.len) >= n, loc=loc) + when !ODIN_NO_BOUNDS_CHECK { + ensure(q.len >= uint(n), "Queue does not have enough elements to consume.", loc) + } if n > 0 { nu := uint(n) q.offset = (q.offset + nu) % builtin.len(q.data) - q.len -= nu + q.len -= nu } } -// Consume `n` elements from the back of the queue +/* +Consume `n` elements from the back of the queue. + +This will raise a bounds checking error if the queue does not have enough elements. +*/ consume_back :: proc(q: ^$Q/Queue($T), n: int, loc := #caller_location) { - assert(condition=int(q.len) >= n, loc=loc) + when !ODIN_NO_BOUNDS_CHECK { + ensure(q.len >= uint(n), "Queue does not have enough elements to consume.", loc) + } if n > 0 { q.len -= uint(n) } @@ -231,9 +450,14 @@ append_elem :: push_back append_elems :: push_back_elems push :: proc{push_back, push_back_elems} append :: proc{push_back, push_back_elems} +enqueue :: push_back +dequeue :: pop_front -// Clear the contents of the queue +/* +Reset the queue's length and offset to zero, letting it write new elements over +old memory, in effect clearing the accessible contents. +*/ clear :: proc(q: ^$Q/Queue($T)) { q.len = 0 q.offset = 0 diff --git a/tests/core/container/queue.odin b/tests/core/container/queue.odin new file mode 100644 index 000000000..56d2e1dca --- /dev/null +++ b/tests/core/container/queue.odin @@ -0,0 +1,157 @@ +package test_core_container + +import "base:runtime" +import "core:container/queue" +import "core:testing" + +@test +test_queue :: proc(t: ^testing.T) { + buf := [?]int{99, 99, 99, 99, 99} + q: queue.Queue(int) + + testing.expect(t, queue.init_from_slice(&q, buf[:])) + testing.expect_value(t, queue.reserve(&q, len(buf)), nil) + + queue.push_back(&q, 1) + queue.push_back_elems(&q, 2, 3) + queue.push_front(&q, 0) + + // { + // data = [1, 2, 3, 99, 0], + // len = 4, + // offset = 4, + // } + + testing.expect_value(t, queue.back(&q), 3) + testing.expect_value(t, queue.back_ptr(&q), &buf[2]) + testing.expect_value(t, queue.front(&q), 0) + testing.expect_value(t, queue.front_ptr(&q), &buf[4]) + + queue.get(&q, 3) + + for i in 0..<4 { + testing.expect_value(t, queue.get(&q, i), i) + queue.set(&q, i, i) + } + testing.expect_value(t, queue.get_ptr(&q, 3), &buf[2]) + + queue.consume_back(&q, 1) + queue.consume_front(&q, 1) + testing.expect_value(t, queue.pop_back(&q), 2) + v, ok := queue.pop_back_safe(&q) + testing.expect_value(t, v, 1) + testing.expect_value(t, ok, true) + + + // Test `init_with_contents`. + buf2 := [?]int{99, 3, 5} + + queue.init_with_contents(&q, buf2[:]) + push_ok, push_err := queue.push_back(&q, 1) + testing.expect(t, !push_ok) + testing.expect_value(t, push_err, runtime.Allocator_Error.Out_Of_Memory) + push_ok, push_err = queue.push_front(&q, 2) + testing.expect(t, !push_ok) + testing.expect_value(t, push_err, runtime.Allocator_Error.Out_Of_Memory) + + pop_front_v, pop_front_ok := queue.pop_front_safe(&q) + testing.expect(t, pop_front_ok) + testing.expect_value(t, pop_front_v, 99) + + // Re-initialization. + queue.init(&q, 0) + defer queue.destroy(&q) + + queue.push_back_elems(&q, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18) + testing.expect_value(t, queue.len(q), 18) + queue.push_back_elems(&q, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18) + testing.expect_value(t, queue.len(q), 36) + + for i in 1..=18 { + testing.expect_value(t, queue.pop_front(&q), i) + } + for i in 1..=18 { + testing.expect_value(t, queue.pop_front(&q), i) + } +} + +@test +test_queue_grow_edge_case :: proc(t: ^testing.T) { + // Create a situation in which we trigger `q.offset + q.len > n` inside + // `_grow` to evaluate the `copy` behavior. + qq: queue.Queue(int) + queue.init(&qq, 0) + defer queue.destroy(&qq) + + queue.push_back_elems(&qq, 1, 2, 3, 4, 5, 6, 7) + testing.expect_value(t, queue.pop_front(&qq), 1) + testing.expect_value(t, queue.pop_front(&qq), 2) + testing.expect_value(t, queue.pop_front(&qq), 3) + queue.push_back(&qq, 8) + queue.push_back(&qq, 9) + + testing.expect_value(t, qq.len, 6) + testing.expect_value(t, qq.offset, 3) + testing.expect_value(t, len(qq.data), 8) // value contingent on smallest dynamic array capacity on first allocation + + queue.reserve(&qq, 16) + + testing.expect_value(t, queue.len(qq), 6) + for i in 4..=9 { + testing.expect_value(t, queue.pop_front(&qq), i) + } + testing.expect_value(t, queue.len(qq), 0) + + // If we made it to this point without failure, the queue should have + // copied the data into the right place after resizing the backing array. +} + +@test +test_queue_grow_edge_case_2 :: proc(t: ^testing.T) { + // Create a situation in which we trigger `insert_from + insert_to > sz` inside `push_back_elems` + // to evaluate the modified `insert_to` behavior. + qq: queue.Queue(int) + queue.init(&qq, 8) + defer queue.destroy(&qq) + + queue.push_back_elems(&qq, -1, -2, -3, -4, -5, -6, -7) + queue.consume_front(&qq, 3) + queue.push_back_elems(&qq, -8, -9, -10) + + queue.push_back_elems(&qq, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + + testing.expect_value(t, queue.len(qq), 17) + for i in 4..=10 { + testing.expect_value(t, queue.pop_front(&qq), -i) + } + for i in 1..=10 { + testing.expect_value(t, queue.pop_front(&qq), i) + } + testing.expect_value(t, queue.len(qq), 0) +} + +@test +test_queue_shrink :: proc(t: ^testing.T) { + qq: queue.Queue(int) + queue.init(&qq, 8) + defer queue.destroy(&qq) + + queue.push_back_elems(&qq, -1, -2, -3, -4, -5, -6, -7) + queue.consume_front(&qq, 3) + queue.push_back_elems(&qq, -8, -9, -10) + + queue.push_back_elems(&qq, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + + queue.shrink(&qq) + queue.consume_front(&qq, 7) + queue.shrink(&qq) + + for i in 1..=10 { + testing.expect_value(t, queue.pop_front(&qq), i) + } + + buf: [1]int + qq_backed: queue.Queue(int) + queue.init_from_slice(&qq_backed, buf[:]) + queue.shrink(&qq_backed) +}