From 2d1d06899ebe6699ad5e210df8aa351bd607eee7 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 15:58:11 +0300 Subject: [PATCH] ludic.base: tb_reserve and imap_reserve size a table up front; HandlePool (a slot and a generation, nothing made after hd_pool_new, past its capacity a panic naming it); kept_push is the one declared grow, and every growth path left says its bound in @alloc_ok Co-Authored-By: Claude Opus 5.5 --- packages/ludic.base/ecs_grid_file.ludic | 1 + packages/ludic.base/ecs_grid_where.ludic | 6 +- packages/ludic.base/ecs_index.ludic | 2 +- packages/ludic.base/ecs_index_slot.ludic | 7 ++- packages/ludic.base/ecs_map.ludic | 12 ++++ packages/ludic.base/ecs_table.ludic | 18 +----- packages/ludic.base/ecs_table_remove.ludic | 2 +- packages/ludic.base/ecs_table_reserve.ludic | 43 ++++++++++++++ packages/ludic.base/ecs_table_set.ludic | 2 +- packages/ludic.base/handle.ludic | 62 +++++++++++++++++++++ packages/ludic.base/index.ludic | 3 + packages/ludic.base/kept.ludic | 4 ++ packages/ludic.base/queue.ludic | 4 +- packages/ludic.base/system.ludic | 2 + packages/ludic.base/tests/ecs_test.ludic | 18 ++++++ packages/ludic.base/tests/handle_test.ludic | 38 +++++++++++++ packages/ludic.base/text_intern.ludic | 1 + 17 files changed, 199 insertions(+), 26 deletions(-) create mode 100644 packages/ludic.base/ecs_table_reserve.ludic create mode 100644 packages/ludic.base/handle.ludic create mode 100644 packages/ludic.base/kept.ludic create mode 100644 packages/ludic.base/tests/handle_test.ludic diff --git a/packages/ludic.base/ecs_grid_file.ludic b/packages/ludic.base/ecs_grid_file.ludic index 2bdee04b..1d961e74 100644 --- a/packages/ludic.base/ecs_grid_file.ludic +++ b/packages/ludic.base/ecs_grid_file.ludic @@ -29,6 +29,7 @@ function grid_moved(g: Grid, from: int, to: int) -> void { } # twice the buckets, every filed row linked again where its key now hashes +@alloc_ok("past grid_reserve only: the buckets double once past the most rows filed, and are kept") function grid_rehash(g: Grid) -> void { g.mask = g.mask * 2 + 1 g.head = words(g.mask + 1) diff --git a/packages/ludic.base/ecs_grid_where.ludic b/packages/ludic.base/ecs_grid_where.ludic index f92c735f..7ee8559c 100644 --- a/packages/ludic.base/ecs_grid_where.ludic +++ b/packages/ludic.base/ecs_grid_where.ludic @@ -63,7 +63,7 @@ export function tb_within_recs(tb: Table, g: Grid, x: float, z: float, r: let cells = (ix1 - ix0 + 1) * (iz1 - iz0 + 1) if cells > g.count or cells < 0 { for row in 0 .. len(g.key) { - if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r * r { push(out, tb.rec[row]) } + if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r * r { kept_push(out, tb.rec[row]) } } return len(out) } @@ -78,7 +78,7 @@ function grid_recs_cell(tb: Table, g: Grid, xs: floats, zs: floats, mcol: var r = g.head[grid_bucket(g, ix, iz)] while r > 0 { let row = r - 1 - if g.key[row] == k and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r2 { push(out, tb.rec[row]) } + if g.key[row] == k and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r2 { kept_push(out, tb.rec[row]) } r = g.next[row] } } @@ -95,7 +95,7 @@ export function tb_within_of_recs(tb: Table, g: Grid, ix: IntIndex, x: flo for k in 0 .. n { let row = rows[k] if g.gate >= 0 and tb.i[g.gate][row] == 0 { continue } - if grid_d2(xs, zs, row, x, z) <= r * r { push(out, tb.rec[row]) } + if grid_d2(xs, zs, row, x, z) <= r * r { kept_push(out, tb.rec[row]) } } return len(out) } diff --git a/packages/ludic.base/ecs_index.ludic b/packages/ludic.base/ecs_index.ludic index bf0f4cc3..ab193162 100644 --- a/packages/ludic.base/ecs_index.ludic +++ b/packages/ludic.base/ecs_index.ludic @@ -55,7 +55,7 @@ function ix_file(ix: IntIndex, r: int, v: int) -> void { if s < 0 { return } ix.pos[r] = len(ix.lists[s]) ix.val[r] = s - push(ix.lists[s], r) + kept_push(ix.lists[s], r) } # out of its list: the list's last row takes its place diff --git a/packages/ludic.base/ecs_index_slot.ludic b/packages/ludic.base/ecs_index_slot.ludic index c4c2a4a5..c2c92bac 100644 --- a/packages/ludic.base/ecs_index_slot.ludic +++ b/packages/ludic.base/ecs_index_slot.ludic @@ -4,6 +4,7 @@ const IX_DENSE: int = 1024 # a value below this is its own slot; above, one is given it const IX_CAP: int = 16 # rows a value's list holds before it grows: a list grown in play is a new block +@alloc_ok("once per distinct value: its list is made the first time the value is filed, and kept") function ix_list_new() -> words { let l = words(IX_CAP) List.clear(l) @@ -16,15 +17,15 @@ function ix_slot(ix: IntIndex, v: int, make: bool) -> int { if v < IX_DENSE { if v >= len(ix.lists) { if not make { return -1 } - while len(ix.lists) <= v { push(ix.lists, ix_list_new()) } + while len(ix.lists) <= v { kept_push(ix.lists, ix_list_new()) } } return v } var s = imap_get(ix.sparse, v, -1) if s < 0 and make { - while len(ix.lists) < IX_DENSE { push(ix.lists, ix_list_new()) } + while len(ix.lists) < IX_DENSE { kept_push(ix.lists, ix_list_new()) } s = len(ix.lists) - push(ix.lists, ix_list_new()) + kept_push(ix.lists, ix_list_new()) imap_put(ix.sparse, v, s) } return s diff --git a/packages/ludic.base/ecs_map.ludic b/packages/ludic.base/ecs_map.ludic index ff2fca74..c80b7fe5 100644 --- a/packages/ludic.base/ecs_map.ludic +++ b/packages/ludic.base/ecs_map.ludic @@ -69,6 +69,18 @@ export function imap_del(m: IntMap, k: int) -> bool { export function imap_len(m: IntMap) -> int { return m.n } export function imap_clear(m: IntMap) -> void { imap_alloc(m, m.mask + 1) } +# room for `n` keys before the map grows, made now +export function imap_reserve(m: IntMap, n: int) -> void { + if (m.mask + 1) * 3 >= n * 4 { return } + var cap = m.mask + 1 + while cap * 3 < n * 4 { cap = cap * 2 } + let ok = m.keys + let ov = m.vals + imap_alloc(m, cap) + for i in 0 .. len(ok) { if ok[i] >= 0 { imap_put(m, ok[i], ov[i]) } } +} + +@alloc_ok("past imap_reserve only: doubles when half full; the old arrays are not given back") function imap_grow(m: IntMap) -> void { let ok = m.keys let ov = m.vals diff --git a/packages/ludic.base/ecs_table.ludic b/packages/ludic.base/ecs_table.ludic index 83f18337..704a12d2 100644 --- a/packages/ludic.base/ecs_table.ludic +++ b/packages/ludic.base/ecs_table.ludic @@ -66,22 +66,10 @@ export function tb_alive(tb: Table, h: int) -> bool { return tb_row(tb, h) # a new row holding `r`, every column 0 and filed in no index until a setter says where it is export function tb_add(tb: Table, r: T) -> int { var s = 0 - if len(tb.free) > 0 { s = List.pop(tb.free) } else { - s = len(tb.slot_gen) - push(tb.slot_gen, 0) - push(tb.slot_row, -1) - } + if len(tb.free) > 0 { s = List.pop(tb.free) } else { s = tb_new_slot(tb) } tb.slot_row[s] = tb.n let h = s | (tb.slot_gen[s] << 22) - push(tb.rec, r) - push(tb.ent, h) - push(tb.added, tb.tick) - push(tb.changed, tb.tick) - for c in 0 .. len(tb.f) { push(tb.f[c], 0.0) } - for c in 0 .. len(tb.i) { push(tb.i[c], 0) } - tb.n += 1 - for g in 0 .. len(tb.grids) { grid_grow(tb.grids[g]) } - for k in 0 .. len(tb.idx) { ix_grow(tb.idx[k]) } + tb_push_row(tb, r, h) tb_stamp(tb, tb.n - 1) for k in 0 .. len(tb.on_add) { tb.on_add[k].run(h) } return h @@ -91,6 +79,6 @@ export function tb_add(tb: Table, r: T) -> int { function tb_stamp(tb: Table, row: int) -> void { tb.changed[row] = tb.tick let b = row >> 6 - while len(tb.blk) <= b { push(tb.blk, 0) } + while len(tb.blk) <= b { kept_push(tb.blk, 0) } tb.blk[b] = tb.tick } diff --git a/packages/ludic.base/ecs_table_remove.ludic b/packages/ludic.base/ecs_table_remove.ludic index d7e0ccc7..22030cc6 100644 --- a/packages/ludic.base/ecs_table_remove.ludic +++ b/packages/ludic.base/ecs_table_remove.ludic @@ -13,7 +13,7 @@ export function tb_remove(tb: Table, h: int) -> bool { let s = h & ECS_SLOT tb.slot_row[s] = -1 tb.slot_gen[s] = (tb.slot_gen[s] + 1) & ECS_GEN - push(tb.free, s) + kept_push(tb.free, s) return true } diff --git a/packages/ludic.base/ecs_table_reserve.ludic b/packages/ludic.base/ecs_table_reserve.ludic new file mode 100644 index 00000000..2bc2a61b --- /dev/null +++ b/packages/ludic.base/ecs_table_reserve.ludic @@ -0,0 +1,43 @@ +# ludic.base/ecs_table_reserve.ludic - room made at the start: a table sized for its busiest moment +# never grows in play, and nothing that grows is ever given back. What does grow past it is here +export function tb_reserve(tb: Table, rows: int) -> void { + let k = rows - tb.n + if k <= 0 { return } + for j in 0 .. k { tb_push_row(tb, null, 0) } + for j in 0 .. k { tb_pop(tb) } + let s0 = len(tb.slot_gen) + for j in s0 .. rows { + push(tb.slot_gen, 0) + push(tb.slot_row, -1) + push(tb.free, 0) + } + for j in s0 .. rows { + List.pop(tb.slot_gen) + List.pop(tb.slot_row) + List.pop(tb.free) + } + while len(tb.blk) <= (rows >> 6) { push(tb.blk, 0) } + for g in 0 .. len(tb.grids) { grid_reserve(tb.grids[g], rows) } +} + +# a row on every column, grid and index +@alloc_ok("past tb_reserve's rows only: a table grows to its high-water once and keeps it") +function tb_push_row(tb: Table, r: T, h: int) -> void { + push(tb.rec, r) + push(tb.ent, h) + push(tb.added, tb.tick) + push(tb.changed, tb.tick) + for c in 0 .. len(tb.f) { push(tb.f[c], 0.0) } + for c in 0 .. len(tb.i) { push(tb.i[c], 0) } + tb.n += 1 + for g in 0 .. len(tb.grids) { grid_grow(tb.grids[g]) } + for k in 0 .. len(tb.idx) { ix_grow(tb.idx[k]) } +} + +@alloc_ok("past tb_reserve's rows only: a slot is made once and reused through the free list") +function tb_new_slot(tb: Table) -> int { + let s = len(tb.slot_gen) + push(tb.slot_gen, 0) + push(tb.slot_row, -1) + return s +} diff --git a/packages/ludic.base/ecs_table_set.ludic b/packages/ludic.base/ecs_table_set.ludic index 8edb8bf8..1da9ff34 100644 --- a/packages/ludic.base/ecs_table_set.ludic +++ b/packages/ludic.base/ecs_table_set.ludic @@ -59,7 +59,7 @@ export function tb_changed_since(tb: Table, since: int, out: words) -> int if tb.blk[b] <= since { continue } var end = b * 64 + 64 if end > tb.n { end = tb.n } - for r in b * 64 .. end { if tb.changed[r] > since { push(out, r) } } + for r in b * 64 .. end { if tb.changed[r] > since { kept_push(out, r) } } } return len(out) } diff --git a/packages/ludic.base/handle.ludic b/packages/ludic.base/handle.ludic new file mode 100644 index 00000000..ea8435e7 --- /dev/null +++ b/packages/ludic.base/handle.ludic @@ -0,0 +1,62 @@ +# ludic.base/handle.ludic - ids a pool hands out, each a slot and a generation (Table's encoding): a +# freed id's slot is reused under a new generation, so an id kept past its free misses with -1 +export property HandlePool { + name: string = "" + gen: words = null # slot -> generation + live: words = null # slot -> 1 while handed out + free: words = null + n: int = 0 # slots made so far + count: int = 0 # handed out now + refused: int = 0 # asks past the capacity + strict: bool = true # past the capacity is a panic naming the pool, else -1 +} + +# everything the pool will ever hold, made now: nothing after this allocates +export function hd_pool_new(name: string, cap: int) -> HandlePool { + let p = new HandlePool + p.name = name + p.gen = words(cap) + p.live = words(cap) + p.free = words(cap) + List.clear(p.free) + return p +} + +export function hd_new(p: HandlePool) -> int { + var s = -1 + if len(p.free) > 0 { s = List.pop(p.free) } + else if p.n < len(p.gen) { + s = p.n + p.n += 1 + } else { return hd_refuse(p) } + p.live[s] = 1 + p.count += 1 + return s | (p.gen[s] << 22) +} + +function hd_refuse(p: HandlePool) -> int { + p.refused += 1 + if p.strict { panic(`handle pool {p.name} is full at {len(p.gen)}`) } + return ECS_NONE +} + +# the slot a handle names, or -1 when it was freed (or its slot has been handed out again since) +export function hd_slot(p: HandlePool, h: int) -> int { + if h < 0 { return -1 } + let s = h & ECS_SLOT + if s >= p.n or p.live[s] == 0 or p.gen[s] != ((h >> 22) & ECS_GEN) { return -1 } + return s +} + +export function hd_free(p: HandlePool, h: int) -> bool { + let s = hd_slot(p, h) + if s < 0 { return false } + p.live[s] = 0 + p.gen[s] = (p.gen[s] + 1) & ECS_GEN + kept_push(p.free, s) + p.count -= 1 + return true +} + +export function hd_live(p: HandlePool) -> int { return p.count } +export function hd_cap(p: HandlePool) -> int { return len(p.gen) } diff --git a/packages/ludic.base/index.ludic b/packages/ludic.base/index.ludic index c8b94d8e..201eadfe 100644 --- a/packages/ludic.base/index.ludic +++ b/packages/ludic.base/index.ludic @@ -3,6 +3,8 @@ module ludic_base uses numbers float import "tick.ludic" +import "kept.ludic" +import "handle.ludic" import "queue.ludic" import "text_buf.ludic" import "text_intern.ludic" @@ -14,6 +16,7 @@ import "system.ludic" import "ecs_table.ludic" import "ecs_hooks.ludic" import "ecs_table_remove.ludic" +import "ecs_table_reserve.ludic" import "ecs_table_set.ludic" import "ecs_grid.ludic" import "ecs_grid_file.ludic" diff --git a/packages/ludic.base/kept.ludic b/packages/ludic.base/kept.ludic new file mode 100644 index 00000000..a060f646 --- /dev/null +++ b/packages/ludic.base/kept.ludic @@ -0,0 +1,4 @@ +# ludic.base/kept.ludic - the one push the frame is allowed: into a list its owner keeps and clears, +# which grows to the most it has ever held and stays there (a queue's halves, a query's out list) +@alloc_ok("a kept list: it grows to the most it has held once, is cleared and reused, never dropped") +export function kept_push(xs: []T, v: T) -> void { push(xs, v) } diff --git a/packages/ludic.base/queue.ludic b/packages/ludic.base/queue.ludic index 3119d603..fbee553e 100644 --- a/packages/ludic.base/queue.ludic +++ b/packages/ludic.base/queue.ludic @@ -30,7 +30,7 @@ export function queue_new(name: string) -> Queue { } export function q_push(q: Queue, v: T) -> void { - push(q.items, v) + kept_push(q.items, v) q.tag.pending = len(q.items) } @@ -65,7 +65,7 @@ export function q_unheld(q: Queue, pool: []T, out: []T) -> int { var held = false for j in 0 .. len(q.items) { if q.items[j] == r { held = true } } for j in 0 .. len(q.spare) { if q.spare[j] == r { held = true } } - if not held { push(out, r) } + if not held { kept_push(out, r) } } return len(out) } diff --git a/packages/ludic.base/system.ludic b/packages/ludic.base/system.ludic index da96e181..2796bd6d 100644 --- a/packages/ludic.base/system.ludic +++ b/packages/ludic.base/system.ludic @@ -17,6 +17,8 @@ export property System { export open registry Systems of System as SYS +# the declared systems; a registry is not filled when a state's defaults are made, so on first ask +@alloc_ok("once: the runner's list is made the first time it is asked, and kept") function cs_all(base_st: mut BaseState) -> []System { if base_st.cs_list == null { base_st.cs_list = new []System diff --git a/packages/ludic.base/tests/ecs_test.ludic b/packages/ludic.base/tests/ecs_test.ludic index fd9d5731..8d2b7e08 100644 --- a/packages/ludic.base/tests/ecs_test.ludic +++ b/packages/ludic.base/tests/ecs_test.ludic @@ -179,8 +179,26 @@ program EcsTest { test "observers hear each add, and a remove once" (hook_st: HookState) { hooks_case(hook_st) } test "change detection skips the blocks nobody wrote" () { blocks_case() } test "a removed handle misses, rows stay dense, a reused slot is a new handle" () { handles_case() } + function reserve_case() -> void { + let tb: Table = table_new(2, 2) + let g = tb_grid(tb, CX, CZ, ON, 8.0) + let ix = tb_index(tb, KIND, ON) + put(tb, "a", 1.0, 1.0, 3) + tb_reserve(tb, 200) + expect_eq(tb_len(tb), 1) + var hs = new []int + for i in 0 .. 150 { push(hs, put(tb, "t", float(i), 2.0, i % 4)) } + expect_eq(tb_len(tb), 151) + expect_eq(ix_count(ix, 3), 38) + expect(tb_row(tb, hs[10]) >= 0) + expect(tb_remove(tb, hs[10])) + expect_eq(tb_row(tb, hs[10]), -1) + expect_eq(grid_count(g), 150) + } + test "the grid answers nearest and within, gated, after moves" () { grid_case() } test "the kind index follows the setters, the gate and removals" () { index_case() } test "change ticks name the rows written since" () { ticks_case() } test "IntMap puts, finds, deletes and grows" () { map_case() } + test "a reserved table holds its rows and indexes as an unreserved one does" () { reserve_case() } } diff --git a/packages/ludic.base/tests/handle_test.ludic b/packages/ludic.base/tests/handle_test.ludic new file mode 100644 index 00000000..264770aa --- /dev/null +++ b/packages/ludic.base/tests/handle_test.ludic @@ -0,0 +1,38 @@ +# handle_test.ludic - a pool's ids: a freed one misses once its slot is reused, the generation wraps, +# and past the capacity a pool that is not strict refuses rather than grows +import "ludic.base" +program HandleTest { + numbers float + test "a freed handle misses, and its slot comes back under a new generation" { + let p = hd_pool_new("bufs", 4) + p.strict = false + let a = hd_new(p) + let b = hd_new(p) + expect_eq(hd_slot(p, a), 0) + expect_eq(hd_slot(p, b), 1) + expect(hd_free(p, a)) + expect_eq(hd_slot(p, a), -1) + expect(not hd_free(p, a)) + let c = hd_new(p) + expect_eq(hd_slot(p, c), 0) + expect(c != a) + expect_eq(hd_slot(p, a), -1) + expect_eq(hd_live(p), 2) + } + test "the generation wraps after 512 reuses and the pool never grows past its capacity" { + let p = hd_pool_new("once", 2) + p.strict = false + var h = hd_new(p) + let first = h + for i in 0 .. 511 { + hd_free(p, h) + h = hd_new(p) + } + hd_free(p, h) + expect_eq(hd_new(p), first) + hd_new(p) + expect_eq(hd_new(p), -1) + expect_eq(p.refused, 1) + expect_eq(hd_cap(p), 2) + } +} diff --git a/packages/ludic.base/text_intern.ludic b/packages/ludic.base/text_intern.ludic index 62d4bc49..c9ee9434 100644 --- a/packages/ludic.base/text_intern.ludic +++ b/packages/ludic.base/text_intern.ludic @@ -34,6 +34,7 @@ function sb_same(sb: StrBuf, s: string) -> bool { } # the table's string for the buffer's bytes +@alloc_ok("intern: one string per distinct text, up to the table's most, then kept") export function sb_intern(sb: StrBuf, tb: StrTable) -> string { let h = sb_hash(sb) let mask = len(tb.hs) - 1