From 9e73c2019747b8f20fab3e4d95f8f6c2f253dd95 Mon Sep 17 00:00:00 2001 From: Mihail Moskov <28909106+corleypc@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:21:23 +0300 Subject: [PATCH] adds the pop procedure family for soa arrays --- base/runtime/core_builtin.odin | 15 +++++ base/runtime/core_builtin_soa.odin | 63 ++++++++++++++++++- tests/core/runtime/test_core_runtime.odin | 74 +++++++++++++++++++++-- 3 files changed, 145 insertions(+), 7 deletions(-) diff --git a/base/runtime/core_builtin.odin b/base/runtime/core_builtin.odin index 4913b53d9..56037f500 100644 --- a/base/runtime/core_builtin.odin +++ b/base/runtime/core_builtin.odin @@ -215,6 +215,8 @@ remove_range_fixed_capacity_dynamic_array :: proc(array: ^$D/[dynamic; $N]$E, #a unordered_remove :: proc{ unordered_remove_dynamic_array, unordered_remove_fixed_capacity_dynamic_array, + + unordered_remove_soa, } @@ -222,6 +224,8 @@ unordered_remove :: proc{ ordered_remove :: proc{ ordered_remove_dynamic_array, ordered_remove_fixed_capacity_dynamic_array, + + ordered_remove_soa, } @builtin @@ -271,6 +275,8 @@ pop_fixed_capacity_dynamic_array :: proc(array: ^$T/[dynamic; $N]$E, loc := #cal pop :: proc{ pop_dynamic_array, pop_fixed_capacity_dynamic_array, + + pop_soa, } // `pop_safe_dynamic_array` trys to remove and return the end value of dynamic array `array` and reduces the length of `array` by 1. @@ -303,6 +309,8 @@ pop_safe_fixed_capacity_dynamic_array :: proc "contextless" (array: ^$T/[dynamic pop_safe :: proc{ pop_safe_dynamic_array, pop_safe_fixed_capacity_dynamic_array, + + pop_safe_soa, } @@ -342,6 +350,8 @@ pop_front_fixed_capacity_dynamic_array :: proc(array: ^$T/[dynamic; $N]$E, loc : pop_front :: proc{ pop_front_dynamic_array, pop_front_fixed_capacity_dynamic_array, + + pop_front_soa, } @@ -381,6 +391,8 @@ pop_front_safe_fixed_capacity_dynamic_array :: proc "contextless" (array: ^$T/[d pop_front_safe :: proc { pop_front_safe_dynamic_array, pop_front_safe_fixed_capacity_dynamic_array, + + pop_front_safe_soa, } @@ -1212,6 +1224,9 @@ inject_at :: proc{ inject_at_elem_fixed_capacity_dynamic_array, inject_at_elems_fixed_capacity_dynamic_array, inject_at_elem_string_fixed_capacity_dynamic_array, + + inject_at_elem_soa, + inject_at_elems_soa, } diff --git a/base/runtime/core_builtin_soa.odin b/base/runtime/core_builtin_soa.odin index 61b93efd7..d355e3cee 100644 --- a/base/runtime/core_builtin_soa.odin +++ b/base/runtime/core_builtin_soa.odin @@ -56,7 +56,7 @@ Raw_SOA_Footer_Dynamic_Array :: struct { // Multipointer indexing lowers to GEP and doesn't capture, so prefer that throughout. @(builtin, require_results) -raw_soa_footer_slice :: proc(array: ^$T/#soa[]$E) -> (footer: ^Raw_SOA_Footer_Slice) { +raw_soa_footer_slice :: proc "contextless" (array: ^$T/#soa[]$E) -> (footer: ^Raw_SOA_Footer_Slice) { if array == nil { return nil } @@ -65,7 +65,7 @@ raw_soa_footer_slice :: proc(array: ^$T/#soa[]$E) -> (footer: ^Raw_SOA_Footer_Sl return } @(builtin, require_results) -raw_soa_footer_dynamic_array :: proc(array: ^$T/#soa[dynamic]$E) -> (footer: ^Raw_SOA_Footer_Dynamic_Array) { +raw_soa_footer_dynamic_array :: proc "contextless" (array: ^$T/#soa[dynamic]$E) -> (footer: ^Raw_SOA_Footer_Dynamic_Array) { if array == nil { return nil } @@ -704,6 +704,58 @@ into_dynamic_soa :: proc(array: $T/#soa[]$E) -> #soa[dynamic]E { return d } +// `pop_soa` will remove and return the end value of the #soa dynamic array `array` and reduces the length of `array` by 1. +// +// Note: If the #soa dynamic array has no elements (`len(array) == 0`), this procedure will panic. +@builtin +pop_soa :: proc(#no_alias array: ^$T/#soa[dynamic]$E, loc := #caller_location) -> (res: E) #no_bounds_check { + assert(len(array) > 0, loc=loc) + res = array[len(array)-1] + raw_soa_footer_dynamic_array(array).len -= 1 + return +} + +// `pop_safe_soa` trys to remove and return the end value of the #soa dynamic array `array` and reduces the length of `array` by 1. +// If the operation is not possible, it will return false. +@builtin +pop_safe_soa :: proc "contextless" (#no_alias array: ^$T/#soa[dynamic]$E) -> (res: E, ok: bool) #no_bounds_check { + if len(array) == 0 { + return + } + res, ok = array[len(array)-1], true + raw_soa_footer_dynamic_array(array).len -= 1 + return +} + +// `pop_front_soa` will remove and return the first value of the #soa dynamic array `array` and reduces the length of `array` by 1, +// whilst keeping the order of the other elements. +// +// Note: This is an O(N) operation. +// Note: If the #soa dynamic array has no elements (`len(array) == 0`), this procedure will panic. +@builtin +pop_front_soa :: proc(#no_alias array: ^$T/#soa[dynamic]$E, loc := #caller_location) -> (res: E) #no_bounds_check { + assert(len(array) > 0, loc=loc) + res = array[0] + _ordered_remove_soa(array, 0) + return +} + +// `pop_front_safe_soa` trys to remove and return the first value of the #soa dynamic array `array` and reduces the +// length of `array` by 1, whilst keeping the order of the other elements. +// If the operation is not possible, it will return false. +// +// Note: This is an O(N) operation. +@builtin +pop_front_safe_soa :: proc "contextless" (#no_alias array: ^$T/#soa[dynamic]$E) -> (res: E, ok: bool) #no_bounds_check { + if len(array) == 0 { + return + } + + res, ok = array[0], true + _ordered_remove_soa(array, 0) + return +} + // `unordered_remove_soa` 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 dynamic array by 1. // @@ -714,7 +766,6 @@ into_dynamic_soa :: proc(array: $T/#soa[]$E) -> #soa[dynamic]E { unordered_remove_soa :: proc(#no_alias array: ^$T/#soa[dynamic]$E, #any_int index: int, loc := #caller_location) #no_bounds_check { bounds_check_error_loc(loc, index, len(array)) if index+1 < len(array) { - // Use the compiler's #soa element load and store lowering. array[index] = array[len(array)-1] } raw_soa_footer_dynamic_array(array).len -= 1 @@ -728,6 +779,12 @@ unordered_remove_soa :: proc(#no_alias array: ^$T/#soa[dynamic]$E, #any_int inde @builtin ordered_remove_soa :: proc(#no_alias array: ^$T/#soa[dynamic]$E, #any_int index: int, loc := #caller_location) #no_bounds_check { bounds_check_error_loc(loc, index, len(array)) + _ordered_remove_soa(array, index) +} + +// the unchecked body of ordered_remove_soa, shared with the front pops. +// index must already be known to be in bounds. +_ordered_remove_soa :: proc "contextless" (#no_alias array: ^$T/#soa[dynamic]$E, index: int) #no_bounds_check { if index+1 < len(array) { ti := type_info_of(typeid_of(T)) ti = type_info_base(ti) diff --git a/tests/core/runtime/test_core_runtime.odin b/tests/core/runtime/test_core_runtime.odin index fafd16674..13b0f02a0 100644 --- a/tests/core/runtime/test_core_runtime.odin +++ b/tests/core/runtime/test_core_runtime.odin @@ -1081,11 +1081,9 @@ test_memory_compare_zero :: proc(t: ^testing.T) { } } -// Runs identical append/inject/remove sequences for a #soa[dynamic] array +// Runs identical append/inject/remove/pop sequences for a #soa[dynamic] array // and an AoS [dynamic] reference, comparing all elements after each -// stage. Covers appends, injections (interior, at the end, and past the -// end where the gap must read as zero elements), unordered and ordered -// removes. +// stage. Covers appends, injections, pops, unordered and ordered removes. @(test) test_soa_array_append_inject_remove :: proc(t: ^testing.T) { check :: proc(t: ^testing.T, $E: typeid, mk: proc(i: int) -> E) { @@ -1242,6 +1240,74 @@ test_soa_array_append_inject_remove :: proc(t: ^testing.T) { testing.expect_value(t, err, nil) non_zero_append(&ref, mk(42)) expect_same(t, soa, ref) + + // interleave pops from both ends + n, err = append(&soa, ..buf[:]) + testing.expect_value(t, n, 32) + testing.expect_value(t, err, nil) + append(&ref, ..buf[:]) + for _ in 0..<8 { + testing.expect_value(t, pop_soa(&soa), pop(&ref)) + testing.expect_value(t, pop_front_soa(&soa), pop_front(&ref)) + } + expect_same(t, soa, ref) + + // safe pops from both ends, down to empty + popped: E + for len(ref) > 0 { + popped, ok = pop_safe_soa(&soa) + testing.expect(t, ok) + testing.expect_value(t, popped, pop(&ref)) + if len(ref) == 0 { + break + } + popped, ok = pop_front_safe_soa(&soa) + testing.expect(t, ok) + testing.expect_value(t, popped, pop_front(&ref)) + } + expect_same(t, soa, ref) + testing.expect_value(t, len(soa), 0) + + // safe pops must report failure on an empty array + _, ok = pop_safe_soa(&soa) + testing.expect(t, !ok) + _, ok = pop_front_safe_soa(&soa) + testing.expect(t, !ok) + + // test procedure groups resolution for the builtin names + for i in 0..<10 { + append(&soa, mk(700 + i)) + append(&ref, mk(700 + i)) + } + ok, err = inject_at(&soa, 3, mk(800)) + testing.expect(t, ok) + testing.expect_value(t, err, nil) + inject_at(&ref, 3, mk(800)) + expect_same(t, soa, ref) + + ok, err = inject_at(&soa, 2, ..buf[:BATCH_LEN]) + testing.expect(t, ok) + testing.expect_value(t, err, nil) + inject_at(&ref, 2, ..buf[:BATCH_LEN]) + expect_same(t, soa, ref) + + ordered_remove(&soa, 4) + ordered_remove(&ref, 4) + unordered_remove(&soa, 1) + unordered_remove(&ref, 1) + expect_same(t, soa, ref) + + testing.expect_value(t, pop(&soa), pop(&ref)) + testing.expect_value(t, pop_front(&soa), pop_front(&ref)) + expect_same(t, soa, ref) + + popped, ok = pop_safe(&soa) + testing.expect(t, ok) + testing.expect_value(t, popped, pop(&ref)) + popped, ok = pop_front_safe(&soa) + testing.expect(t, ok) + testing.expect_value(t, popped, pop_front(&ref)) + expect_same(t, soa, ref) } // mixed field widths + padding