thinking about key tables...

This commit is contained in:
2025-10-21 22:07:55 -04:00
parent 43141183a6
commit 1e18592ff5
11 changed files with 279 additions and 265 deletions

View File

@@ -2,7 +2,10 @@
This prototype aims to flesh out ideas I've wanted to explore futher on code editing & related tooling.
The things to explore:
Current goal with the prototype is just making a good visualizer & note aggregation for codebases & libraries.
My note repos with affine links give an idea of what that would look like.
The things to explore (future):
* 2D canvas for laying out code visualized in various types of ASTs
* WYSIWYG frontend ASTs
@@ -28,55 +31,14 @@ The dependencies are:
* [sokol-odin (Sectr Fork)](https://github.com/Ed94/sokol-odin)
* [sokol-tools](https://github.com/floooh/sokol-tools)
* Powershell (if you want to use my build scripts)
* backtrace (not used yet)
* freetype (not used yet)
* Eventually some config parser (maybe I'll use metadesk, or [ini](https://github.com/laytan/odin-ini-parser))
The project is so far in a "codebase boostrapping" phase. Most the work being done right now is setting up high performance linear zoom rendering for text and UI.
Text has recently hit sufficient peformance targets, and now inital UX has become the focus.
The project's is organized into 2 runtime modules sectr_host & sectr.
The host module loads the main module & its memory. Hot-reloading it's dll when it detects a change.
Codebase organization:
* App: General app config, state, and operations.
* Engine: client interface for host, tick, update, rendering.
* Has the following definitions: startup, shutdown, reload, tick, clean_frame (which host hooks up to when managing the client dll)
* Will handle async ops.
* Font Provider: Manages fonts.
* Bulk of implementation maintained as a separate library: [VEFontCache-Odin](https://github.com/Ed94/VEFontCache-Odin)
* Grime: Name speaks for itself, stuff not directly related to the target features to iterate upon for the prototype.
* Defining dependency aliases or procedure overload tables, rolling own allocator, data structures, etc.
* Input: All human input related features
* Base input features (polling & related) are platform abstracted from sokol_app
* Entirely user rebindable
* Math: The usual for 2D/3D.
* Parsers:
* AST generation, editing, and serialization.
* Parsers for different levels of "synatitic & semantic awareness", Formatting -> Domain Specific AST
* Figure out pragmatic transformations between ASTs.
* Project: Encpasulation of user config/context/state separate from persistent app's
* Manages the codebase (database & model view controller)
* Manages workspaces : View compositions of the codebase
* UI: Core graphic user interface framework, AST visualzation & editing, backend visualization
* PIMGUI (Persistent Immediate Mode User Interface)
* Auto-layout
* Supports heavy procedural generation of box widgets
* Viewports
* Docking/Tiling, Floating, Canvas
Due to the nature of the prototype there are 'sub-groups' such as the codebase being its own ordeal as well as the workspace.
They'll be elaborated in their own documentation
## Gallery
![img](docs/assets/sectr_host_2024-03-09_04-30-27.png)
![img](docs/assets/sectr_host_2024-05-04_12-29-39.png)
![img](docs/assets/Code_2024-05-04_12-55-53.png)
![img](docs/assets/sectr_host_2024-05-11_22-34-15.png)
![img](docs/assets/sectr_host_2024-05-15_03-32-36.png)
![img](docs/assets/Code_2024-05-21_23-15-16.gif)
## Notes

View File

@@ -1,9 +1,20 @@
package grime
hash32_djb8 :: #force_inline proc "contextless" ( hash : ^u32, bytes : []byte ) {
hash32_djb8 :: #force_inline proc "contextless" (hash: ^u32, bytes: []byte ) {
for value in bytes do (hash^) = (( (hash^) << 8) + (hash^) ) + u32(value)
}
hash64_djb8 :: #force_inline proc "contextless" ( hash : ^u64, bytes : []byte ) {
hash64_djb8 :: #force_inline proc "contextless" (hash: ^u64, bytes: []byte ) {
for value in bytes do (hash^) = (( (hash^) << 8) + (hash^) ) + u64(value)
}
// Ripped from core:hash, fnv32a
@(optimization_mode="favor_size")
hash32_fnv1a :: #force_inline proc "contextless" (hash: ^u32, data: []byte, seed := u32(0x811c9dc5)) {
hash^ = seed; for b in data { hash^ = (hash^ ~ u32(b)) * 0x01000193 }
}
@(optimization_mode="favor_size")
hash64_fnv1a :: #force_inline proc "contextless" (hash: ^u64, data: []byte, seed := u64(0xcbf29ce484222325)) {
hash^ = seed; for b in data { hash^ = (hash^ ~ u64(b)) * 0x100000001b3 }
}

View File

@@ -1,164 +0,0 @@
package grime
import "base:intrinsics"
/*
Key Table 1-Layer Chained-Chunked-Cells
*/
KT1CX_Slot :: struct($type: typeid) {
value: type,
key: u64,
occupied: b32,
}
KT1CX_Cell :: struct($type: typeid, $depth: int) {
slots: [depth]KT1CX_Slot(type),
next: ^KT1CX_Cell(type, depth),
}
KT1CX :: struct($cell: typeid) {
table: []cell,
}
KT1CX_Byte_Slot :: struct {
key: u64,
occupied: b32,
}
KT1CX_Byte_Cell :: struct {
next: ^byte,
}
KT1CX_Byte :: struct {
table: []byte,
}
KT1CX_ByteMeta :: struct {
slot_size: int,
slot_key_offset: uintptr,
cell_next_offset: uintptr,
cell_depth: int,
cell_size: int,
type_width: int,
type: typeid,
}
KT1CX_InfoMeta :: struct {
table_size: int,
slot_size: int,
slot_key_offset: uintptr,
cell_next_offset: uintptr,
cell_depth: int,
cell_size: int,
type_width: int,
type: typeid,
}
KT1CX_Info :: struct {
backing_table: AllocatorInfo,
}
kt1cx_init :: proc(info: KT1CX_Info, m: KT1CX_InfoMeta, result: ^KT1CX_Byte) {
assert(result != nil)
assert(info.backing_table.procedure != nil)
assert(m.cell_depth > 0)
assert(m.table_size >= 4 * Kilo)
assert(m.type_width > 0)
table_raw, error := mem_alloc(m.table_size * m.cell_size, ainfo = allocator(info.backing_table))
assert(error == .None); slice_assert(transmute([]byte) table_raw)
(transmute(^SliceByte) & table_raw).len = m.table_size
result.table = table_raw
}
kt1cx_clear :: proc(kt: KT1CX_Byte, m: KT1CX_ByteMeta) {
cell_cursor := cursor(kt.table)
table_len := len(kt.table) * m.cell_size
for ; cell_cursor != end(kt.table); cell_cursor = cell_cursor[m.cell_size:] // for cell, cell_id in kt.table.cells
{
slots := SliceByte { cell_cursor, m.cell_depth * m.slot_size } // slots = cell.slots
slot_cursor := slots.data
for;; {
slot := slice(slot_cursor, m.slot_size) // slot = slots[slot_id]
zero(slot) // slot = {}
if slot_cursor == end(slots) { // if slot == end(slot)
next := slot_cursor[m.cell_next_offset:] // next = kt.table.cells[cell_id + 1]
if next != nil { // if next != nil
slots.data = next // slots = next.slots
slot_cursor = next
continue
}
}
slot_cursor = slot_cursor[m.slot_size:] // slot = slots[slot_id + 1]
}
}
}
kt1cx_slot_id :: proc(kt: KT1CX_Byte, key: u64, m: KT1CX_ByteMeta) -> u64 {
cell_size := m.cell_size // dummy value
hash_index := key % u64(len(kt.table))
return hash_index
}
kt1cx_get :: proc(kt: KT1CX_Byte, key: u64, m: KT1CX_ByteMeta) -> ^byte {
hash_index := kt1cx_slot_id(kt, key, m)
cell_offset := uintptr(hash_index) * uintptr(m.cell_size)
cell_cursor := cursor(kt.table)[cell_offset:] // cell_id = 0
{
slots := slice(cell_cursor, m.cell_depth * m.slot_size) // slots = cell[cell_id].slots
slot_cursor := cell_cursor // slot_id = 0
for;;
{
slot := transmute(^KT1CX_Byte_Slot) slot_cursor[m.slot_key_offset:] // slot = cell[slot_id]
if slot.occupied && slot.key == key {
return cast(^byte) slot_cursor
}
if slot_cursor == end(slots)
{
cell_next := cell_cursor[m.cell_next_offset:] // cell.next
if cell_next != nil {
slots = slice(cell_next, len(slots)) // slots = cell.next
slot_cursor = cell_next
cell_cursor = cell_next // cell = cell.next
continue
}
else {
return nil
}
}
slot_cursor = slot_cursor[m.slot_size:]
}
}
}
kt1cx_set :: proc(kt: KT1CX_Byte, key: u64, value: []byte, backing_cells: Odin_Allocator, m: KT1CX_ByteMeta) -> ^byte {
hash_index := kt1cx_slot_id(kt, key, m)
cell_offset := uintptr(hash_index) * uintptr(m.cell_size)
cell_cursor := cursor(kt.table)[cell_offset:] // KT1CX_Cell(Type) cell = kt.table[hash_index]
{
slots := SliceByte {cell_cursor, m.cell_depth * m.slot_size} // cell.slots
slot_cursor := slots.data
for ;;
{
slot := transmute(^KT1CX_Byte_Slot) slot_cursor[m.slot_key_offset:]
if slot.occupied == false {
slot.occupied = true
slot.key = key
return cast(^byte) slot_cursor
}
else if slot.key == key {
return cast(^byte) slot_cursor
}
if slot_cursor == end(slots) {
curr_cell := transmute(^KT1CX_Byte_Cell) (uintptr(cell_cursor) + m.cell_next_offset) // curr_cell = cell
if curr_cell != nil {
slots.data = curr_cell.next
slot_cursor = curr_cell.next
cell_cursor = curr_cell.next
continue
}
else {
new_cell, _ := mem_alloc(m.cell_size, ainfo = backing_cells)
curr_cell.next = raw_data(new_cell)
slot = transmute(^KT1CX_Byte_Slot) cursor(new_cell)[m.slot_key_offset:]
slot.occupied = true
slot.key = key
return raw_data(new_cell)
}
}
slot_cursor = slot_cursor[m.slot_size:]
}
return nil
}
}
kt1cx_assert :: proc(kt: $type / KT1CX) {
slice_assert(kt.table)
}
kt1cx_byte :: proc(kt: $type / KT1CX) -> KT1CX_Byte { return { slice( transmute([^]byte) cursor(kt.table), len(kt.table)) } }

View File

@@ -1,48 +0,0 @@
package grime
/*
Key Table 1-Layer Linear (KT1L)
*/
KT1L_Slot :: struct($Type: typeid) {
key: u64,
value: Type,
}
KT1L_Meta :: struct {
slot_size: int,
kt_value_offset: int,
type_width: int,
type: typeid,
}
kt1l_populate_slice_a2_Slice_Byte :: proc(kt: ^[]byte, backing: AllocatorInfo, values: []byte, num_values: int, m: KT1L_Meta) {
assert(kt != nil)
if num_values == 0 { return }
table_size_bytes := num_values * int(m.slot_size)
kt^, _ = mem_alloc(table_size_bytes, ainfo = transmute(Odin_Allocator) backing)
slice_assert(kt ^)
kt_raw : SliceByte = transmute(SliceByte) kt^
for id in 0 ..< num_values {
slot_offset := id * m.slot_size // slot id
slot_cursor := kt_raw.data[slot_offset:] // slots[id] type: KT1L_<Type>
// slot_key := transmute(^u64) slot_cursor // slots[id].key type: U64
// slot_value := slice(slot_cursor[m.kt_value_offset:], m.type_width) // slots[id].value type: <Type>
a2_offset := id * m.type_width * 2 // a2 entry id
a2_cursor := cursor(values)[a2_offset:] // a2_entries[id] type: A2_<Type>
// a2_key := (transmute(^[]byte) a2_cursor) ^ // a2_entries[id].key type: <Type>
// a2_value := slice(a2_cursor[m.type_width:], m.type_width) // a2_entries[id].value type: <Type>
mem_copy_non_overlapping(slot_cursor[m.kt_value_offset:], a2_cursor[m.type_width:], m.type_width) // slots[id].value = a2_entries[id].value
(transmute([^]u64) slot_cursor)[0] = 0;
hash64_djb8(transmute(^u64) slot_cursor, (transmute(^[]byte) a2_cursor) ^) // slots[id].key = hash64_djb8(a2_entries[id].key)
}
kt_raw.len = num_values
}
kt1l_populate_slice_a2 :: proc($Type: typeid, kt: ^[]KT1L_Slot(Type), backing: AllocatorInfo, values: [][2]Type) {
assert(kt != nil)
values_bytes := slice(transmute([^]u8) raw_data(values), len(values) * size_of([2]Type))
kt1l_populate_slice_a2_Slice_Byte(transmute(^[]byte) kt, backing, values_bytes, len(values), {
slot_size = size_of(KT1L_Slot(Type)),
kt_value_offset = offset_of(KT1L_Slot(Type), value),
type_width = size_of(Type),
type = Type,
})
}

View File

@@ -0,0 +1,196 @@
package grime
import "base:intrinsics"
/*
Key Table Chained-Chunked-Cells
Table has a cell with a user-specified depth. Each cell will be a linear search if the first slot is occupied.
Table allocated cells are looked up by hash.
If a cell is exhausted additional are allocated singly-chained reporting to the user when it does with a "cell_overflow" counter.
Slots track occupacy with a tombstone (occupied signal).
If the table ever needs to change its size, it should be a wipe and full traversal of the arena holding the values..
or maybe a wipe of that arena as it may no longer be accessible.
Has a likely-hood of having cache misses (based on reading other impls about these kind of tables).
Odin's hash-map or Jai's are designed with open-addressing and prevent that.
Intended to be wrapped in parent interface (such as a string cache). Keys are hashed by the table's user.
The table is not intended to directly store the type's value in it's slots (expects the slot value to be some sort of reference).
The value should be stored in an arena.
Could be upgraded two a X-layer, not sure if its ever viable.
Would essentially be segmenting the hash to address a multi-layered table lookup.
Where one table leads to another hash resolving id for a subtable with linear search of cells after.
*/
KTCX_Slot :: struct($type: typeid) {
value: type,
key: u64,
occupied: b32,
}
KTCX_Cell :: struct($type: typeid, $depth: int) {
slots: [depth]KTCX_Slot(type),
next: ^KTCX_Cell(type, depth),
}
KTCX :: struct($cell: typeid) {
table: []cell,
cell_overflow: int,
}
KTCX_Byte_Slot :: struct {
key: u64,
occupied: b32,
}
KTCX_Byte_Cell :: struct {
next: ^byte,
}
KTCX_Byte :: struct {
table: []byte,
cell_overflow: int,
}
KTCX_ByteMeta :: struct {
slot_size: int,
slot_key_offset: uintptr,
cell_next_offset: uintptr,
cell_depth: int,
cell_size: int,
type_width: int,
type: typeid,
}
KTCX_Info :: struct {
table_size: int,
slot_size: int,
slot_key_offset: uintptr,
cell_next_offset: uintptr,
cell_depth: int,
cell_size: int,
type_width: int,
type: typeid,
}
ktcx_byte :: #force_inline proc "contextless" (kt: $type / KTCX) -> KTCX_Byte { return { slice( transmute([^]byte) cursor(kt.table), len(kt.table)) } }
ktcx_init_byte :: proc(result: ^KTCX_Byte, tbl_backing: Odin_Allocator, m: KTCX_Info) {
assert(result != nil)
assert(tbl_backing.procedure != nil)
assert(m.cell_depth > 0)
assert(m.table_size >= 4 * Kilo)
assert(m.type_width > 0)
table_raw, error := mem_alloc(m.table_size * m.cell_size, ainfo = tbl_backing)
assert(error == .None); slice_assert(transmute([]byte) table_raw)
(transmute(^SliceByte) & table_raw).len = m.table_size
result.table = table_raw
}
ktcx_clear :: proc(kt: KTCX_Byte, m: KTCX_ByteMeta) {
cell_cursor := cursor(kt.table)
table_len := len(kt.table) * m.cell_size
for ; cell_cursor != end(kt.table); cell_cursor = cell_cursor[m.cell_size:] // for cell, cell_id in kt.table.cells
{
slots := SliceByte { cell_cursor, m.cell_depth * m.slot_size } // slots = cell.slots
slot_cursor := slots.data
for;; {
slot := slice(slot_cursor, m.slot_size) // slot = slots[slot_id]
zero(slot) // slot = {}
if slot_cursor == end(slots) { // if slot == end(slot)
next := slot_cursor[m.cell_next_offset:] // next = kt.table.cells[cell_id + 1]
if next != nil { // if next != nil
slots.data = next // slots = next.slots
slot_cursor = next
continue
}
}
slot_cursor = slot_cursor[m.slot_size:] // slot = slots[slot_id + 1]
}
}
}
ktcx_slot_id :: #force_inline proc "contextless" (table: []byte, key: u64) -> u64 {
return key % u64(len(table))
}
ktcx_get :: proc(kt: KTCX_Byte, key: u64, m: KTCX_ByteMeta) -> ^byte {
hash_index := key % u64(len(kt.table)) // ktcx_slot_id
cell_offset := uintptr(hash_index) * uintptr(m.cell_size)
cell_cursor := cursor(kt.table)[cell_offset:] // cell_id = 0
{
slots := slice(cell_cursor, m.cell_depth * m.slot_size) // slots = cell[cell_id].slots
slot_cursor := cell_cursor // slot_id = 0
for;;
{
slot := transmute(^KTCX_Byte_Slot) slot_cursor[m.slot_key_offset:] // slot = cell[slot_id]
if slot.occupied && slot.key == key {
return cast(^byte) slot_cursor
}
if slot_cursor == end(slots)
{
cell_next := cell_cursor[m.cell_next_offset:] // cell.next
if cell_next != nil {
slots = slice(cell_next, len(slots)) // slots = cell.next
slot_cursor = cell_next
cell_cursor = cell_next // cell = cell.next
continue
}
else {
return nil
}
}
slot_cursor = slot_cursor[m.slot_size:]
}
}
}
ktcx_set :: proc(kt: ^KTCX_Byte, key: u64, value: []byte, backing_cells: Odin_Allocator, m: KTCX_ByteMeta) -> ^byte {
hash_index := key % u64(len(kt.table)) // ktcx_slot_id
cell_offset := uintptr(hash_index) * uintptr(m.cell_size)
cell_cursor := cursor(kt.table)[cell_offset:] // KTCX_Cell(Type) cell = kt.table[hash_index]
{
slots := SliceByte {cell_cursor, m.cell_depth * m.slot_size} // cell.slots
slot_cursor := slots.data
for ;;
{
slot := transmute(^KTCX_Byte_Slot) slot_cursor[m.slot_key_offset:]
if slot.occupied == false {
slot.occupied = true
slot.key = key
return cast(^byte) slot_cursor
}
else if slot.key == key {
return cast(^byte) slot_cursor
}
if slot_cursor == end(slots) {
curr_cell := transmute(^KTCX_Byte_Cell) (uintptr(cell_cursor) + m.cell_next_offset) // curr_cell = cell
if curr_cell != nil {
slots.data = curr_cell.next
slot_cursor = curr_cell.next
cell_cursor = curr_cell.next
continue
}
else {
ensure(false, "Exhausted a cell. Increase the table size?")
new_cell, _ := mem_alloc(m.cell_size, ainfo = backing_cells)
curr_cell.next = raw_data(new_cell)
slot = transmute(^KTCX_Byte_Slot) cursor(new_cell)[m.slot_key_offset:]
slot.occupied = true
slot.key = key
kt.cell_overflow += 1
return raw_data(new_cell)
}
}
slot_cursor = slot_cursor[m.slot_size:]
}
return nil
}
}
// Type aware wrappers
ktcx_init :: #force_inline proc(table_size: int, tbl_backing: Odin_Allocator,
kt: ^$kt_type / KTCX(KTCX_Cell(KTCX_Slot($Type), $Depth))
){
ktcx_init_byte(transmute(^KTCX_Byte) kt, tbl_backing, {
table_size = table_size,
slot_size = size_of(KTCX_Slot(Type)),
slot_key_offset = offset_of(KTCX_Slot(Type), key),
cell_next_offset = offset_of(KTCX_Cell(Type, Depth), next),
cell_depth = Depth,
cell_size = size_of(KTCX_Cell(Type, Depth)),
type_width = size_of(Type),
type = Type,
})
}

View File

@@ -0,0 +1,37 @@
package grime
/*
Key Table 1-Layer Linear (KT1L)
Mainly intended for doing linear lookup of key-paried values. IE: Arg value parsing with label ids.
The table is built in one go from the key-value pairs. The default populate slice_a2 has the key and value as the same type.
*/
KTL_Slot :: struct($Type: typeid) {
key: u64,
value: Type,
}
KTL_Meta :: struct {
slot_size: int,
kt_value_offset: int,
type_width: int,
type: typeid,
}
ktl_get :: #force_inline proc(kt: []KTL_Slot($Type), key: u64) -> ^Type {
for & slot in kt { if key == slot.key do return & slot.value; }
return nil
}
// Unique populator for key-value pair strings
ktl_populate_slice_a2_str :: #force_inline proc (kt: ^[]KTL_Slot(string), backing: Odin_Allocator, values: [][2]string) {
assert(kt != nil)
if len(values) == 0 { return }
raw_bytes, error := mem_alloc(size_of(KTL_Slot(string)) * len(values), ainfo = backing); assert(error == .None);
kt^ = slice( transmute([^]KTL_Slot(string)) cursor(raw_bytes), len(raw_bytes) / size_of(KTL_Slot(string)) )
for id in 0 ..< len(values) {
mem_copy_non_overlapping(& kt[id].value, & values[id][1], size_of(string))
hash64_fnv1a(& kt[id].key, transmute([]byte) values[id][0])
}
}

View File

@@ -1,13 +1,14 @@
package grime
/*
Hassh Table based on John's Jai & Sean Barrett's
Hash Table based on John's Jai & Sean Barrett's
I don't like the table definition cntaining
the allocator, hash or compare procedure to be used.
So it has been stripped and instead applied on procedure site,
the parent container or is responsible for tracking that.
TODO(Ed): Resolve appropriate Key-Table term for it.
TODO(Ed): Complete this later if we actually have issues with KT1CX or Odin's map.
*/
KT_Slot :: struct(

View File

@@ -5,6 +5,23 @@ Mega :: Kilo * 1024
Giga :: Mega * 1024
Tera :: Giga * 1024
// Provides the nearest prime number value for the given capacity
closest_prime :: proc(capacity: uint) -> uint
{
prime_table : []uint = {
53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
6291469, 12582917, 25165843, 50331653, 100663319,
201326611, 402653189, 805306457, 1610612741, 3221225473, 6442450941
};
for slot in prime_table {
if slot >= capacity {
return slot
}
}
return prime_table[len(prime_table) - 1]
}
raw_cursor :: #force_inline proc "contextless" (ptr: rawptr) -> [^]byte { return transmute([^]byte) ptr }
ptr_cursor :: #force_inline proc "contextless" (ptr: ^$Type) -> [^]Type { return transmute([^]Type) ptr }

View File

@@ -17,7 +17,7 @@ MemoryTracker :: struct {
entries : Array(MemoryTrackerEntry),
}
Track_Memory :: true
Track_Memory :: false
@(disabled = Track_Memory == false)
memtracker_clear :: proc (tracker: MemoryTracker) {

View File

@@ -7,18 +7,20 @@ StrKey_U4 :: struct {
StrKT_U4_Cell_Depth :: 4
StrKT_U4_Slot :: KT1CX_Slot(StrKey_U4)
StrKT_U4_Cell :: KT1CX_Cell(StrKT_U4_Slot, 4)
StrKT_U4_Table :: KT1CX(StrKT_U4_Cell)
StrKT_U4_Slot :: KTCX_Slot(StrKey_U4)
StrKT_U4_Cell :: KTCX_Cell(StrKT_U4_Slot, 4)
StrKT_U4_Table :: KTCX(StrKT_U4_Cell)
VStrKT_U4 :: struct {
varena: VArena, // Backed by growing vmem
entries: StrKT_U4_Table
varena: VArena, // Backed by growing vmem
kt: StrKT_U4_Table,
}
vstrkt_u4_init :: proc(varena: ^VArena) -> (cache: ^VStrKT_U4)
vstrkt_u4_init :: proc(varena: ^VArena, capacity: int, cache: ^VStrKT_U4)
{
return nil
capacity := cast(int) closest_prime(cast(uint) capacity)
ktcx_init(capacity, varena_allocator(varena), &cache.kt)
return
}
vstrkt_u4_intern :: proc(cache: ^VStrKT_U4) -> StrKey_U4