mirror of
https://github.com/odin-lang/Odin.git
synced 2026-04-22 22:35:19 +00:00
538 lines
15 KiB
Odin
538 lines
15 KiB
Odin
/*
|
|
Exponential Array (Xar).
|
|
|
|
A dynamically growing array using exponentially-sized chunks, providing stable
|
|
memory addresses for all elements. Unlike `[dynamic]T`, elements are never
|
|
moved once allocated, making it safe to hold pointers to elements.
|
|
|
|
For more information: https://azmr.uk/dyn/#exponential-arrayxar
|
|
|
|
Example:
|
|
|
|
import "core:container/xar"
|
|
|
|
example :: proc() {
|
|
x: xar.Array(int, 4)
|
|
defer xar.destroy(&x)
|
|
|
|
xar.push_back(&x, 10)
|
|
xar.push_back(&x, 20)
|
|
xar.push_back(&x, 30)
|
|
|
|
ptr := xar.get_ptr(&x, 1) // ptr remains valid after more push_backs
|
|
xar.push_back(&x, 40)
|
|
fmt.println(ptr^) // prints 20
|
|
}
|
|
*/
|
|
package container_xar
|
|
|
|
@(require) import "base:intrinsics"
|
|
@(require) import "base:runtime"
|
|
|
|
PLATFORM_BITS :: 8*size_of(uint)
|
|
_LOG2_PLATFORM_BITS :: intrinsics.constant_log2(PLATFORM_BITS)
|
|
|
|
MAX_SHIFT :: PLATFORM_BITS>>1
|
|
|
|
/*
|
|
An Exponential Array with stable element addresses.
|
|
|
|
Unlike `[dynamic]T` which reallocates and moves elements when growing, `Array`
|
|
allocates separate chunks of exponentially increasing size. This guarantees
|
|
that pointers to elements remain valid for the lifetime of the container.
|
|
|
|
Fields:
|
|
- `chunks`: Fixed array of multi-pointers to allocated chunks
|
|
- `len`: Number of elements currently stored
|
|
- `allocator`: Allocator used for chunk allocations
|
|
|
|
Type Parameters:
|
|
- `T`: The element type
|
|
- `SHIFT`: Controls initial chunk size (1 << SHIFT). Must be in range (0, MAX_SHIFT].
|
|
Larger values mean fewer, bigger chunks. Recommended: 4-8.
|
|
|
|
Chunk sizes grow as:
|
|
- `chunks[0]`: 1 << SHIFT elements
|
|
- `chunks[1]`: 1 << SHIFT elements
|
|
- `chunks[2]`: 1 << (SHIFT + 1) elements
|
|
- `chunks[3]`: 1 << (SHIFT + 2) elements
|
|
- `chunks[4]`: 1 << (SHIFT + 3) elements
|
|
- ...and so on
|
|
|
|
Example:
|
|
|
|
import "core:container/xar"
|
|
|
|
example :: proc() {
|
|
// Xar with initial chunk size of 16 (1 << 4)
|
|
x: xar.Array(My_Struct, 4)
|
|
defer xar.destroy(&x)
|
|
}
|
|
*/
|
|
Array :: struct($T: typeid, $SHIFT: uint) where 0 < SHIFT, SHIFT <= MAX_SHIFT {
|
|
chunks: [(1 << (_LOG2_PLATFORM_BITS - intrinsics.constant_log2(SHIFT))) + 1][^]T,
|
|
len: int,
|
|
allocator: runtime.Allocator,
|
|
}
|
|
|
|
|
|
/*
|
|
Initializes an exponential array with the given allocator.
|
|
|
|
**Inputs**
|
|
- `x`: Pointer to the exponential array to initialize
|
|
- `allocator`: Allocator to use for chunk allocations (defaults to context.allocator)
|
|
*/
|
|
array_init :: proc(x: ^$X/Array($T, $SHIFT), allocator := context.allocator) {
|
|
x^ = {allocator = allocator}
|
|
}
|
|
|
|
/*
|
|
Frees all allocated chunks and resets the exponential array.
|
|
|
|
**Inputs**
|
|
- `x`: Pointer to the exponential array to destroy
|
|
*/
|
|
array_destroy :: proc(x: ^$X/Array($T, $SHIFT)) {
|
|
#reverse for c, i in x.chunks {
|
|
if c != nil {
|
|
n := 1 << (SHIFT + uint(i if i > 0 else 1) - 1)
|
|
size_in_bytes := n * size_of(T)
|
|
runtime.mem_free_with_size(c, size_in_bytes, x.allocator)
|
|
}
|
|
}
|
|
x^ = {}
|
|
}
|
|
|
|
/*
|
|
Resets the array's length to zero without freeing memory.
|
|
Allocated chunks are retained for reuse.
|
|
*/
|
|
array_clear :: proc "contextless" (x: ^$X/Array($T, $SHIFT)) {
|
|
x.len = 0
|
|
}
|
|
|
|
// Returns the length of the exponential-array
|
|
@(require_results)
|
|
array_len :: proc "contextless" (x: $X/Array($T, $SHIFT)) -> int {
|
|
return x.len
|
|
}
|
|
|
|
// Returns the number of allocated elements
|
|
@(require_results)
|
|
array_cap :: proc "contextless" (x: $X/Array($T, $SHIFT)) -> int {
|
|
#reverse for c, i in x.chunks {
|
|
if c != nil {
|
|
return 1 << (SHIFT + uint(i if i > 0 else 1))
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Internal: computes chunk index, element index within chunk, and chunk capacity for a given index.
|
|
@(require_results)
|
|
_meta_get :: #force_inline proc "contextless" ($SHIFT: uint, index: uint) -> (chunk_idx, elem_idx, chunk_cap: uint) {
|
|
elem_idx = index
|
|
chunk_cap = uint(1) << SHIFT
|
|
chunk_idx = 0
|
|
|
|
index_shift := index >> SHIFT
|
|
if index_shift > 0 {
|
|
N :: 8*size_of(uint)-1
|
|
CLZ :: intrinsics.count_leading_zeros
|
|
chunk_idx = N-CLZ(index_shift) // MSB(index_shift)
|
|
|
|
chunk_cap = 1 << (chunk_idx + SHIFT)
|
|
elem_idx -= chunk_cap
|
|
chunk_idx += 1
|
|
}
|
|
|
|
return
|
|
}
|
|
/*
|
|
Get a copy of the element at the specified index.
|
|
|
|
**Inputs**
|
|
- `x`: Pointer to the exponential array
|
|
- `index`: Position of the element (0-indexed)
|
|
|
|
**Returns**
|
|
- a copy of the element
|
|
*/
|
|
@(require_results)
|
|
array_get :: proc(x: ^$X/Array($T, $SHIFT), #any_int index: int, loc := #caller_location) -> (val: T) #no_bounds_check {
|
|
runtime.bounds_check_error_loc(loc, index, x.len)
|
|
chunk_idx, elem_idx, _ := _meta_get(SHIFT, uint(index))
|
|
return x.chunks[chunk_idx][elem_idx]
|
|
}
|
|
|
|
/*
|
|
Get a pointer to the element at the specified index.
|
|
|
|
The returned pointer remains valid even after additional elements are added,
|
|
as long as the element is not removed and the array is not destroyed.
|
|
|
|
**Inputs**
|
|
- `x`: Pointer to the exponential array
|
|
- `index`: Position of the element (0-indexed)
|
|
|
|
**Returns**
|
|
- a stable pointer to the element
|
|
|
|
Example:
|
|
|
|
import "core:container/xar"
|
|
|
|
get_ptr_example :: proc() {
|
|
x: xar.Array(int, 4)
|
|
defer xar.destroy(&x)
|
|
|
|
xar.push_back(&x, 100)
|
|
ptr := xar.get_ptr(&x, 0)
|
|
|
|
// Pointer remains valid after growing
|
|
for i in 0..<1000 {
|
|
xar.push_back(&x, i)
|
|
}
|
|
|
|
fmt.println(ptr^) // Still prints 100
|
|
}
|
|
*/
|
|
@(require_results)
|
|
array_get_ptr :: proc(x: ^$X/Array($T, $SHIFT), #any_int index: int, loc := #caller_location) -> (val: ^T) #no_bounds_check {
|
|
runtime.bounds_check_error_loc(loc, index, x.len)
|
|
chunk_idx, elem_idx, _ := _meta_get(SHIFT, uint(index))
|
|
return &x.chunks[chunk_idx][elem_idx]
|
|
}
|
|
|
|
// No bounds checking
|
|
@(require_results)
|
|
array_get_ptr_unsafe :: proc "contextless" (x: ^$X/Array($T, $SHIFT), #any_int index: int) -> (val: ^T) #no_bounds_check {
|
|
chunk_idx, elem_idx, _ := _meta_get(SHIFT, uint(index))
|
|
return &x.chunks[chunk_idx][elem_idx]
|
|
}
|
|
|
|
/*
|
|
Set the element at the specified index to the given value.
|
|
|
|
**Inputs**
|
|
- `x`: Pointer to the exponential array
|
|
- `index`: Position of the element (0-indexed)
|
|
- `value`: The value to set
|
|
*/
|
|
array_set :: proc(x: ^$X/Array($T, $SHIFT), #any_int index: int, value: T, loc := #caller_location) #no_bounds_check {
|
|
runtime.bounds_check_error_loc(loc, index, x.len)
|
|
chunk_idx, elem_idx, _ := _meta_get(SHIFT, uint(index))
|
|
x.chunks[chunk_idx][elem_idx] = value
|
|
}
|
|
|
|
array_append :: proc{array_push_back_elem, array_push_back_elems}
|
|
array_push_back :: proc{array_push_back_elem, array_push_back_elems}
|
|
|
|
|
|
/*
|
|
Append an element to the end of the exponential array.
|
|
Allocates a new chunk if necessary. Existing elements aren't moved, and their pointers remain stable.
|
|
|
|
**Inputs**
|
|
- `x`: Pointer to the exponential array
|
|
- `value`: The element to append
|
|
|
|
**Returns**
|
|
- number of elements added (always 1 on success)
|
|
- allocation error if chunk allocation failed
|
|
|
|
Example:
|
|
|
|
import "core:container/xar"
|
|
|
|
push_back_example :: proc() {
|
|
x: xar.Array(string, 4)
|
|
defer xar.destroy(&x)
|
|
|
|
xar.push_back(&x, "hello")
|
|
xar.push_back(&x, "world")
|
|
|
|
fmt.println(xar.get(&x, 0)) // hello
|
|
fmt.println(xar.get(&x, 1)) // world
|
|
}
|
|
*/
|
|
array_push_back_elem :: proc(x: ^$X/Array($T, $SHIFT), value: T, loc := #caller_location) -> (n: int, err: runtime.Allocator_Error) {
|
|
if x.allocator.procedure == nil {
|
|
// to minic `[dynamic]T` behaviour
|
|
x.allocator = context.allocator
|
|
}
|
|
|
|
chunk_idx, elem_idx, chunk_cap := _meta_get(SHIFT, uint(x.len))
|
|
if x.chunks[chunk_idx] == nil {
|
|
x.chunks[chunk_idx] = make([^]T, chunk_cap, x.allocator) or_return
|
|
}
|
|
x.chunks[chunk_idx][elem_idx] = value
|
|
x.len += 1
|
|
n = 1
|
|
return
|
|
}
|
|
|
|
/*
|
|
Append multiple elements to the end of the exponential array.
|
|
|
|
**Inputs**
|
|
- `x`: Pointer to the exponential array
|
|
- `values`: The elements to append
|
|
|
|
**Returns**
|
|
- number of elements successfully added
|
|
- allocation error if chunk allocation failed (partial append possible)
|
|
*/
|
|
array_push_back_elems :: proc(x: ^$X/Array($T, $SHIFT), values: ..T, loc := #caller_location) -> (n: int, err: runtime.Allocator_Error) {
|
|
for value in values {
|
|
n += array_push_back_elem(x, value, loc) or_return
|
|
}
|
|
return
|
|
}
|
|
|
|
array_append_and_get_ptr :: array_push_back_elem_and_get_ptr
|
|
append_and_get_ptr :: array_push_back_elem_and_get_ptr
|
|
|
|
/*
|
|
Append an element and return a stable pointer to it.
|
|
This is useful when you need to initialize a complex struct in-place or
|
|
retain a reference to the newly added element.
|
|
|
|
**Inputs**
|
|
- `x`: Pointer to the exponential array
|
|
- `value`: The element to append
|
|
|
|
**Returns**
|
|
- a stable pointer to the newly added element
|
|
- allocation error if chunk allocation failed
|
|
|
|
Example:
|
|
|
|
import "core:container/xar"
|
|
|
|
push_back_and_get_ptr_example :: proc() {
|
|
x: xar.Array(My_Struct, 4)
|
|
defer xar.destroy(&x)
|
|
|
|
ptr := xar.push_back_elem_and_get_ptr(&x, My_Struct{}) or_else panic("alloc failed")
|
|
ptr.field = 42 // Initialize in-place
|
|
}
|
|
*/
|
|
@(require_results)
|
|
array_push_back_elem_and_get_ptr :: proc(x: ^$X/Array($T, $SHIFT), value: T, loc := #caller_location) -> (ptr: ^T, err: runtime.Allocator_Error) {
|
|
if x.allocator.procedure == nil {
|
|
// to minic `[dynamic]T` behaviour
|
|
x.allocator = context.allocator
|
|
}
|
|
|
|
chunk_idx, elem_idx, chunk_cap := _meta_get(SHIFT, uint(x.len))
|
|
if x.chunks[chunk_idx] == nil {
|
|
x.chunks[chunk_idx] = make([^]T, chunk_cap, x.allocator) or_return
|
|
}
|
|
x.chunks[chunk_idx][elem_idx] = value
|
|
x.len += 1
|
|
ptr = &x.chunks[chunk_idx][elem_idx]
|
|
return
|
|
}
|
|
|
|
// `pop` will remove and return the end value of an exponential array `x` and reduces the length of the array by 1.
|
|
//
|
|
// Note: If the exponential array has no elements (`xar.len(x) == 0`), this procedure will panic.
|
|
array_pop :: proc(x: ^$X/Array($T, $SHIFT), loc := #caller_location) -> (val: T) {
|
|
assert(x.len > 0, loc=loc)
|
|
index := uint(x.len-1)
|
|
chunk_idx, elem_idx, _ := _meta_get(SHIFT, index)
|
|
x.len -= 1
|
|
return x.chunks[chunk_idx][elem_idx]
|
|
}
|
|
|
|
// `pop_safe` trys to remove and return the end value of dynamic array `x` and reduces the length of the array by 1.
|
|
// If the operation is not possible, it will return false.
|
|
@(require_results)
|
|
array_pop_safe :: proc(x: ^$X/Array($T, $SHIFT)) -> (val: T, ok: bool) {
|
|
if x.len == 0 {
|
|
return
|
|
}
|
|
index := uint(x.len-1)
|
|
chunk_idx, elem_idx, _ := _meta_get(SHIFT, index)
|
|
x.len -= 1
|
|
|
|
val = x.chunks[chunk_idx][elem_idx]
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
/*
|
|
`unordered_remove` removed the element at the specified `index`. It does so by replacing the current end value
|
|
with the old value, and reducing the length of the exponential array by 1.
|
|
|
|
Note: This is an O(1) operation.
|
|
Note: This is currently no procedure that is the equivalent of an "ordered_remove"
|
|
Note: If the index is out of bounds, this procedure will panic.
|
|
|
|
Note: Pointers to the last element become invalid (it gets moved). Pointers to other elements remain valid.
|
|
|
|
Example:
|
|
|
|
import "core:encoding/xar"
|
|
|
|
unordered_remove_example :: proc() {
|
|
x: xar.Array(int, 4)
|
|
defer xar.destroy(&x)
|
|
|
|
xar.push_back(&x, 10)
|
|
xar.push_back(&x, 20)
|
|
xar.push_back(&x, 30)
|
|
|
|
xar.unordered_remove(&x, 0) // Removes 10, replaces with 30
|
|
|
|
// Array now contains [30, 20]
|
|
fmt.println(xar.get(&x, 0)) // 30
|
|
fmt.println(xar.get(&x, 1)) // 20
|
|
}
|
|
*/
|
|
array_unordered_remove :: proc(x: ^$X/Array($T, $SHIFT), #any_int index: int, loc := #caller_location) {
|
|
runtime.bounds_check_error_loc(loc, index, x.len)
|
|
n := x.len-1
|
|
if index != n {
|
|
end := array_get(x, n)
|
|
array_set(x, index, end)
|
|
}
|
|
x.len -= 1
|
|
}
|
|
|
|
@(require_results)
|
|
array_linear_search :: proc(x: ^$X/Array($T, $SHIFT), elem: T) -> (index: int, found: bool) where intrinsics.type_is_comparable(T) {
|
|
it := array_iterator(x)
|
|
for val, i in array_iterate_by_val(it) {
|
|
if val == elem {
|
|
return i, true
|
|
}
|
|
}
|
|
return -1, false
|
|
}
|
|
|
|
|
|
/*
|
|
Iterator state for traversing a `Xar`.
|
|
|
|
Fields:
|
|
- `xar`: Pointer to the exponential array being iterated
|
|
- `idx`: Current iteration index
|
|
*/
|
|
Array_Iterator :: struct($T: typeid, $SHIFT: uint) {
|
|
xar: ^Array(T, SHIFT),
|
|
idx: int,
|
|
}
|
|
|
|
/*
|
|
Create an iterator for traversing the exponential array.
|
|
|
|
**Inputs**
|
|
- `xar`: Pointer to the exponential array
|
|
|
|
**Returns**
|
|
- an iterator positioned at the start
|
|
|
|
Example:
|
|
|
|
import "core:container/xar"
|
|
import "core:fmt"
|
|
|
|
iterator_example :: proc() {
|
|
x: xar.Array(int, 4)
|
|
defer xar.destroy(&x)
|
|
|
|
xar.push_back(&x, 10)
|
|
xar.push_back(&x, 20)
|
|
xar.push_back(&x, 30)
|
|
|
|
it := xar.iterator(&x)
|
|
for val in xar.iterate_by_ptr(&it) {
|
|
fmt.println(val^)
|
|
}
|
|
}
|
|
|
|
Output:
|
|
|
|
10
|
|
20
|
|
30
|
|
*/
|
|
array_iterator :: proc(xar: ^$X/Array($T, $SHIFT)) -> Array_Iterator(T, SHIFT) {
|
|
return {xar = auto_cast xar, idx = 0}
|
|
}
|
|
|
|
/*
|
|
Advance the iterator and returns the next element.
|
|
|
|
**Inputs**
|
|
- `it`: Pointer to the iterator
|
|
|
|
**Returns**
|
|
- current element
|
|
- `true` if an element was returned, `false` if iteration is complete
|
|
*/
|
|
array_iterate_by_val :: proc(it: ^Array_Iterator($T, $SHIFT)) -> (val: T, idx: int, ok: bool) {
|
|
if it.idx >= it.xar.len {
|
|
it.idx = 0
|
|
return
|
|
}
|
|
val = array_get(it.xar, it.idx)
|
|
idx = it.idx
|
|
it.idx += 1
|
|
return val, idx, true
|
|
}
|
|
|
|
|
|
/*
|
|
Advance the iterator and returns a pointer to the next element.
|
|
|
|
**Inputs**
|
|
- `it`: Pointer to the iterator
|
|
|
|
**Returns**
|
|
- pointer to the current element
|
|
- `true` if an element was returned, `false` if iteration is complete
|
|
*/
|
|
array_iterate_by_ptr :: proc(it: ^Array_Iterator($T, $SHIFT)) -> (val: ^T, idx: int, ok: bool) {
|
|
if it.idx >= it.xar.len {
|
|
it.idx = 0
|
|
return
|
|
}
|
|
val = array_get_ptr(it.xar, it.idx)
|
|
idx = it.idx
|
|
it.idx += 1
|
|
return val, idx, true
|
|
}
|
|
|
|
|
|
|
|
|
|
init :: proc{array_init, freelist_init}
|
|
destroy :: proc{array_destroy, freelist_destroy}
|
|
clear :: proc{array_clear, freelist_clear}
|
|
len :: proc{array_len, freelist_len}
|
|
cap :: proc{array_cap, freelist_cap}
|
|
get :: proc{array_get, freelist_get}
|
|
get_ptr_unsafe :: proc{array_get_ptr_unsafe}
|
|
get_ptr :: proc{array_get_ptr, freelist_get_ptr}
|
|
set :: proc{array_set, freelist_set}
|
|
append :: proc{array_push_back_elem, array_push_back_elems}
|
|
push_back :: proc{array_push_back_elem, array_push_back_elems}
|
|
push_back_elem :: proc{array_push_back_elem}
|
|
push_back_elems :: proc{array_push_back_elems}
|
|
push_back_elem_and_get_ptr:: proc{array_push_back_elem_and_get_ptr}
|
|
pop :: proc{array_pop, freelist_pop}
|
|
pop_safe :: proc{array_pop_safe}
|
|
unordered_remove :: proc{array_unordered_remove}
|
|
iterator :: proc{array_iterator, freelist_iterator}
|
|
iterate_by_val :: proc{array_iterate_by_val, freelist_iterate_by_val}
|
|
iterate_by_ptr :: proc{array_iterate_by_ptr, freelist_iterate_by_ptr}
|
|
|
|
push_with_index :: proc{freelist_push_with_index}
|
|
push :: proc{freelist_push}
|
|
release :: proc{freelist_release}
|
|
linear_search :: proc{array_linear_search, freelist_linear_search}
|
|
is_freed :: proc{freelist_is_freed} |