Programming Language

Nocter

A self-contained systems language built around simplicity, encapsulation, and foolproof design.

/development/std/internal/table/growth.nct

growth.nct

//! Failure-atomic bucket rebuilding and capacity management.

see ./index.nct
see ./storage.nct
see ./probing.nct

use /hash.Hash
use /internal/mem.{allocation_abort, checked_add}
use /internal/ptr.replace_value
use /vec.Vec

func reserve_destination(
    buckets: &+Vec<Bucket>,
    new_start: usize,
    new_count: usize,
    hash: u64,
    entry_index: usize,
): void {
    var destination = hash as usize % new_count
    loop {
        let bucket_index = new_start + destination
        let available = match buckets[bucket_index] {
            Bucket.empty { true }
            _ { false }
        }
        if available {
            let _ = replace_value(
                &+buckets[bucket_index],
                Bucket.occupied(entry_index),
            )
            return
        }
        destination = next_index(destination, new_count)
    }
}

func try_rebuild<K, V>(table: &+Table<K, V>, new_count: usize): void! where K impl Hash {
    let old_count = table.buckets.len()

    // The only fallible operation finishes before published bucket state changes.
    // Vec grows through its owned allocator, preserving table affinity.
    table.buckets.try_reserve(new_count)?
    var appended: usize = 0
    while appended < new_count {
        table.buckets.push(Bucket.empty)
        appended += 1
    }

    // The appended range is a complete new bucket table. Entries remain in their
    // dense owner, so rebuilding never moves, copies, or destroys user values.
    var entry_index: usize = 0
    while entry_index < table.keys.len() {
        let hash = hash_key(&table.seed, &table.keys[entry_index])
        reserve_destination(
            &+table.buckets,
            old_count,
            new_count,
            hash,
            entry_index,
        )
        entry_index += 1
    }

    var destination: usize = 0
    while destination < new_count {
        let moved = replace_value(
            &+table.buckets[old_count + destination],
            Bucket.empty,
        )
        let _ = replace_value(&+table.buckets[destination], move moved)
        destination += 1
    }
    table.buckets.truncate(new_count)
    table.usable_bucket_capacity = usable_capacity(new_count)
    table.tombstones = 0
    return
}

func try_reserve_table<K, V>(table: &+Table<K, V>, additional: usize): void! where K impl Hash {
    let required = checked_add(table.keys.len(), additional) otherwise {
        return capacity_overflow()
    }
    if required <= table_capacity(table) {
        return
    }

    // Every partial reserve remains hidden by the minimum-capacity contract. Bucket
    // rebuilding never moves keys or values, and publication happens only after all
    // three stores can hold the requested logical entry count.
    if required > table.usable_bucket_capacity {
        let new_count = bucket_count_for(required, true)?
        try_rebuild(table, new_count)?
    }
    table.keys.try_reserve(additional)?
    table.values.try_reserve(additional)?
    return
}

func cleanup_sparse<K, V>(table: &+Table<K, V>): void where K impl Hash {
    if table.tombstones < 8 || table.tombstones <= table.keys.len() {
        return
    }
    let bucket_count = table.buckets.len()
    try_rebuild(table, bucket_count) catch _ {
        // Cleanup is an optimization. A failed temporary allocation does not turn an
        // otherwise capacity-sufficient insertion into failure.
        return
    }
    return
}

instance Table<K, V> where K impl Hash {
    method &+self.reserve(additional: usize): void {
        try_reserve_table(self, additional) catch _ {
            return allocation_abort()
        }
        return
    }

    method &+self.try_reserve(additional: usize): void! {
        try_reserve_table(self, additional)?
        return
    }
}