From 27cd508571a8d7c9668147a602225231d5690e63 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Wed, 11 Jun 2025 07:05:48 -0400 Subject: [PATCH 1/9] container/queue: Fix and add more bounds checking --- core/container/queue/queue.odin | 42 ++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/core/container/queue/queue.odin b/core/container/queue/queue.odin index d1040a7c9..43eb14410 100644 --- a/core/container/queue/queue.odin +++ b/core/container/queue/queue.odin @@ -80,36 +80,48 @@ reserve :: proc(q: ^$Q/Queue($T), capacity: int) -> runtime.Allocator_Error { 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 { +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] } -front_ptr :: proc(q: ^$Q/Queue($T)) -> ^T { +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] } -back :: proc(q: ^$Q/Queue($T)) -> T { +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] } -back_ptr :: proc(q: ^$Q/Queue($T)) -> ^T { +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] } 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)) + runtime.bounds_check_error_loc(loc, i, int(q.len)) idx := (uint(i)+q.offset)%builtin.len(q.data) return &q.data[idx] @@ -152,7 +164,9 @@ push_front :: proc(q: ^$Q/Queue($T), elem: T) -> (ok: bool, err: runtime.Allocat // Pop an element from the back of the queue 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] @@ -171,7 +185,9 @@ pop_back_safe :: proc(q: ^$Q/Queue($T)) -> (elem: T, ok: bool) { // Pop an element from the front of the queue 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 @@ -209,7 +225,9 @@ push_back_elems :: proc(q: ^$Q/Queue($T), elems: ..T) -> (ok: bool, err: runtime // Consume `n` elements from the front of the queue 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) @@ -219,7 +237,9 @@ consume_front :: proc(q: ^$Q/Queue($T), n: int, loc := #caller_location) { // Consume `n` elements from the back of the queue 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) } From 58bda1209a7aa261523b64935b62d80dc0877727 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Wed, 11 Jun 2025 07:07:19 -0400 Subject: [PATCH 2/9] container/queue: Deprecate `peek_*` The `*_ptr` and `peek_*` procedures did the same thing, except `peek_*` was over-cautiously putting the index through a modulo when all assignments to `q.offset` are already wrapped. --- core/container/queue/queue.odin | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/container/queue/queue.odin b/core/container/queue/queue.odin index 43eb14410..9e9dec2ec 100644 --- a/core/container/queue/queue.odin +++ b/core/container/queue/queue.odin @@ -127,16 +127,14 @@ get_ptr :: proc(q: ^$Q/Queue($T), #any_int i: int, loc := #caller_location) -> ^ 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 From 862442511a2684adecafb3688f2b7ad172a3a47d Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Wed, 11 Jun 2025 07:23:05 -0400 Subject: [PATCH 3/9] container/queue: Reorganize --- core/container/queue/queue.odin | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/core/container/queue/queue.odin b/core/container/queue/queue.odin index 9e9dec2ec..c58da3e13 100644 --- a/core/container/queue/queue.odin +++ b/core/container/queue/queue.odin @@ -86,12 +86,27 @@ get :: proc(q: ^$Q/Queue($T), #any_int i: int, loc := #caller_location) -> T { return q.data[idx] } +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 :: proc(q: ^$Q/Queue($T), #any_int i: int, val: T, loc := #caller_location) { + runtime.bounds_check_error_loc(loc, i, int(q.len)) + + idx := (uint(i)+q.offset)%builtin.len(q.data) + q.data[idx] = val +} + 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] } + front_ptr :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> ^T { when !ODIN_NO_BOUNDS_CHECK { ensure(q.len > 0, "Queue is empty.", loc) @@ -114,18 +129,6 @@ back_ptr :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> ^T { return &q.data[idx] } -set :: proc(q: ^$Q/Queue($T), #any_int i: int, val: T, loc := #caller_location) { - 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, int(q.len)) - - idx := (uint(i)+q.offset)%builtin.len(q.data) - return &q.data[idx] -} @(deprecated="Use `front_ptr` instead") peek_front :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> ^T { From 6cb84e467bd9ea4b7ebf36640a192fe6e3e00fd8 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Wed, 11 Jun 2025 08:05:27 -0400 Subject: [PATCH 4/9] container/queue: Document the package --- core/container/queue/queue.odin | 215 ++++++++++++++++++++++++++++---- 1 file changed, 193 insertions(+), 22 deletions(-) diff --git a/core/container/queue/queue.odin b/core/container/queue/queue.odin index c58da3e13..dd22a13d0 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,7 +19,9 @@ 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 @@ -22,9 +30,17 @@ init :: proc(q: ^$Q/Queue($T), capacity := DEFAULT_CAPACITY, allocator := contex 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 +52,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,27 +72,45 @@ 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)) @@ -78,7 +118,11 @@ reserve :: proc(q: ^$Q/Queue($T), capacity: int) -> runtime.Allocator_Error { return nil } +/* +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, int(q.len)) @@ -86,6 +130,11 @@ get :: proc(q: ^$Q/Queue($T), #any_int i: int, loc := #caller_location) -> T { return q.data[idx] } +/* +Get a pointer to the element at index `i`. + +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)) @@ -93,6 +142,11 @@ get_ptr :: proc(q: ^$Q/Queue($T), #any_int i: int, loc := #caller_location) -> ^ 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, int(q.len)) @@ -100,6 +154,11 @@ set :: proc(q: ^$Q/Queue($T), #any_int i: int, val: T, loc := #caller_location) q.data[idx] = val } +/* +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) @@ -107,6 +166,11 @@ front :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> T { 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) @@ -114,6 +178,11 @@ front_ptr :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> ^T { 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) @@ -121,6 +190,12 @@ back :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> T { 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) @@ -140,7 +215,30 @@ peek_back :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> ^T { 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 @@ -151,7 +249,30 @@ 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 @@ -162,8 +283,30 @@ push_front :: proc(q: ^$Q/Queue($T), elem: T) -> (ok: bool, err: runtime.Allocat 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) { when !ODIN_NO_BOUNDS_CHECK { ensure(q.len > 0, "Queue is empty.", loc) @@ -173,7 +316,11 @@ pop_back :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> (elem: T) { 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 @@ -184,7 +331,11 @@ 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) { when !ODIN_NO_BOUNDS_CHECK { ensure(q.len > 0, "Queue is empty.", loc) @@ -194,7 +345,11 @@ pop_front :: proc(q: ^$Q/Queue($T), loc := #caller_location) -> (elem: T) { 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] @@ -205,7 +360,12 @@ 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) { @@ -224,7 +384,11 @@ 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) { when !ODIN_NO_BOUNDS_CHECK { ensure(q.len >= uint(n), "Queue does not have enough elements to consume.", loc) @@ -236,7 +400,11 @@ consume_front :: proc(q: ^$Q/Queue($T), n: int, loc := #caller_location) { } } -// 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) { when !ODIN_NO_BOUNDS_CHECK { ensure(q.len >= uint(n), "Queue does not have enough elements to consume.", loc) @@ -254,7 +422,10 @@ push :: proc{push_back, push_back_elems} append :: proc{push_back, push_back_elems} -// 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 From 81f57634820279443abb7cc4fd3465477201e167 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Wed, 11 Jun 2025 08:05:46 -0400 Subject: [PATCH 5/9] container/queue: Add common aliases `enqueue` and `dequeue` --- core/container/queue/queue.odin | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/container/queue/queue.odin b/core/container/queue/queue.odin index dd22a13d0..75b00d376 100644 --- a/core/container/queue/queue.odin +++ b/core/container/queue/queue.odin @@ -420,6 +420,8 @@ 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 /* From 040d79e1b99d952907167b9688b776920ad1d300 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Wed, 11 Jun 2025 08:15:50 -0400 Subject: [PATCH 6/9] container/queue: Let queues be re-initialized with different allocators --- core/container/queue/queue.odin | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/container/queue/queue.odin b/core/container/queue/queue.odin index 75b00d376..7e3e18075 100644 --- a/core/container/queue/queue.odin +++ b/core/container/queue/queue.odin @@ -23,10 +23,13 @@ DEFAULT_CAPACITY :: 16 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) } From 638a1529a30c4dbcf54b4e65b7f9c8fa034cba1c Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:36:57 -0400 Subject: [PATCH 7/9] container/queue: Add `shrink` --- core/container/queue/queue.odin | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/core/container/queue/queue.odin b/core/container/queue/queue.odin index 7e3e18075..5c7369a50 100644 --- a/core/container/queue/queue.odin +++ b/core/container/queue/queue.odin @@ -121,6 +121,33 @@ reserve :: proc(q: ^$Q/Queue($T), capacity: int) -> runtime.Allocator_Error { 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`. From 66b2acbf2431b01fe9835e5b7f443009950390dc Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:04:38 -0400 Subject: [PATCH 8/9] container/queue: Add tests --- tests/core/container/queue.odin | 157 ++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 tests/core/container/queue.odin 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) +} From 23c1ce8722943bc59087151d5f62a6fc5acb8a32 Mon Sep 17 00:00:00 2001 From: Feoramund <161657516+Feoramund@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:57:38 -0400 Subject: [PATCH 9/9] container/queue: Remove trailing whitespace --- core/container/queue/queue.odin | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/container/queue/queue.odin b/core/container/queue/queue.odin index 5c7369a50..7f6f55826 100644 --- a/core/container/queue/queue.odin +++ b/core/container/queue/queue.odin @@ -116,7 +116,7 @@ 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 } @@ -167,7 +167,7 @@ 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] } @@ -179,7 +179,7 @@ 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, int(q.len)) - + idx := (uint(i)+q.offset)%builtin.len(q.data) q.data[idx] = val } @@ -306,7 +306,7 @@ Example: 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 @@ -401,7 +401,7 @@ push_back_elems :: proc(q: ^$Q/Queue($T), elems: ..T) -> (ok: bool, err: runtime 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 @@ -426,7 +426,7 @@ consume_front :: proc(q: ^$Q/Queue($T), n: int, loc := #caller_location) { if n > 0 { nu := uint(n) q.offset = (q.offset + nu) % builtin.len(q.data) - q.len -= nu + q.len -= nu } }