diff --git a/lib/std/collections/flatmap.c3 b/lib/std/collections/flatmap.c3 new file mode 100644 index 000000000..29eeaa5c0 --- /dev/null +++ b/lib/std/collections/flatmap.c3 @@ -0,0 +1,518 @@ +<* + @require types::has_equals(Key) : "Must have equality operator overloaded" + @require $defined((Key) { }.hash()) : "Must be hashable" +*> +module std::collections::flatmap ; +import std::io; +import std::math; +import std::collections::pair; + +alias HashType = uint; +const sz DEFAULT_CAPACITY @private = 16; +const float DEFAULT_LOAD_FACTOR @private = 0.85f; +const HashType HASH_EMPTY @private = 0; +const HashType HASH_DELETED @private = 1; +const HashType HASH_FIRST_VALID @private = 2; + +<* + FlatMap is an open-addressing map that keeps every entry in one contiguous, + cache-friendly block, unlike separate chaining where each key is a separate + heap node linked from its bucket. The flat design is memory-efficient and + fast to iterate and probe, but fills under load: the table periodically + rebuilds into a larger block, and collisions or delete/re-add churn degrade + probing performance. +*> +struct FlatMap +{ + HashType* data; + Allocator alloc; + sz count; + sz capacity; + float load_factor; +} + +<* + @param [&inout] allocator : "The allocator to use" + @require capacity > 0 : "The capacity must be 1 or higher" + @require load_factor > 0.0 && load_factor <= 1 : "The load factor must be higher than 0 and less or equal to 1" + @require !self.is_initialized() : "Map was already initialized" +*> +fn FlatMap* FlatMap.init( + &self, + Allocator allocator, + sz capacity = DEFAULT_CAPACITY, + float load_factor = DEFAULT_LOAD_FACTOR +) +{ + capacity = math::next_power_of_2(capacity); + sz mem_req = calc_mem_req(capacity); + self.data = alloc::malloc(allocator, mem_req); + self.capacity = capacity; + self.count = 0; + self.alloc = allocator; + self.load_factor = load_factor; + reset_hashes(self.data[:self.capacity]); + return self; +} + +<* + @require capacity > 0 : "The capacity must be 1 or higher" + @require load_factor > 0.0 && load_factor <= 1 : "The load factor must be higher than 0 and less or equal to 1" + @require !self.is_initialized() : "Map was already initialized" +*> +fn FlatMap* FlatMap.tinit(&self, sz capacity = DEFAULT_CAPACITY, float load_factor = DEFAULT_LOAD_FACTOR) +{ + return self.init(tmem, capacity, load_factor); +} + +<* + @param [in] keys : "The keys for the FlatMap entries" + @param [in] values : "The values for the FlatMap entries" + @param [&inout] allocator : "The allocator to use" + @require keys.len == values.len : "Both keys and values arrays must be the same length" + @require capacity > 0 : "The capacity must be 1 or higher" + @require load_factor > 0.0 && load_factor <= 1 : "The load factor must be higher than 0 and less or equal to 1" + @require !self.is_initialized() : "Map was already initialized" +*> +fn FlatMap* FlatMap.init_from_keys_and_values( + &self, + Allocator allocator, + Key[] keys, + Value[] values, + sz capacity = DEFAULT_CAPACITY, + float load_factor = DEFAULT_LOAD_FACTOR +) +{ + self.init(allocator, capacity, load_factor); + foreach (idx, key : keys) + { + self.set(key, values[idx]); + } + return self; +} + +<* + @param [in] keys : "The keys for the FlatMap entries" + @param [in] values : "The values for the FlatMap entries" + @require keys.len == values.len : "Both keys and values arrays must be the same length" + @require capacity > 0 : "The capacity must be 1 or higher" + @require load_factor > 0.0 && load_factor <= 1 : "The load factor must be higher than 0 and less or equal to 1" + @require !self.is_initialized() : "Map was already initialized" + @require keys.len == values.len : "Arrays must have equal length" +*> +fn FlatMap* FlatMap.tinit_from_keys_and_values( + &self, + Key[] keys, + Value[] values, + sz capacity = DEFAULT_CAPACITY, + float load_factor = DEFAULT_LOAD_FACTOR +) +{ + return self.init_from_keys_and_values(tmem, keys, values, capacity, load_factor); +} + +<* + Frees all underlying resources. + Shouldn't be used after this operation without initialization. +*> +fn void FlatMap.free(&self) +{ + if (self.data) + { + alloc::free(self.alloc, self.data); + *self = { }; + } +} + +<* + Resets underlying data, so flat map can be reused. +*> +fn void FlatMap.clear(&self) +{ + self.count = 0; + HashType[] hashes = self.data[:self.capacity]; + reset_hashes(hashes); +} + +<* + Inserts key-value pair into the flat map. +*> +fn void FlatMap.set(&self, Key key, Value val) +{ + if (crossed_threshold(self)) grow(self); + HashType hash = calc_hash(key); + set_inner(self.data, self.capacity, &self.count, hash, key, val); +} + +fn void set_inner(HashType* data, sz capacity, sz* count, HashType hash, Key key, Value val) @private @inline +{ + sz start_idx = get_idx(hash, capacity); + sz curr_idx = start_idx; + Pair{Key[], Value[]} kv = keys_values_array(data, capacity); + Key[] keys = kv.first; + Value[] values = kv.second; + + for (; curr_idx < capacity; ++curr_idx) + { + if (data[curr_idx] < HASH_FIRST_VALID) + { + data[curr_idx] = hash; + keys[curr_idx] = key; + values[curr_idx] = val; + if (count) (*count)++; + return; + } + else if (hash == data[curr_idx] && keys[curr_idx] == key) + { + data[curr_idx] = hash; + keys[curr_idx] = key; + values[curr_idx] = val; + return; + } + } + + // Wrapped around, start from first entry. + curr_idx = 0; + + for (; curr_idx < start_idx; ++curr_idx) + { + if (data[curr_idx] < HASH_FIRST_VALID) + { + data[curr_idx] = hash; + keys[curr_idx] = key; + values[curr_idx] = val; + if (count) (*count)++; + return; + } + else if (hash == data[curr_idx] && keys[curr_idx] == key) + { + data[curr_idx] = hash; + keys[curr_idx] = key; + values[curr_idx] = val; + return; + } + } + + unreachable("Failed to insert a value into a hashmap"); +} + +<* + Get reference to the value associated with the provided key. +*> +fn Value*? FlatMap.get_ref(&self, Key key) +{ + HashType search_hash = calc_hash(key); + sz start_idx = get_idx(search_hash, self.capacity); + sz curr_idx = start_idx; + Pair{Key[], Value[]} kv = keys_values_array(self.data, self.capacity); + Key[] keys = kv.first; + Value[] values = kv.second; + + for (; curr_idx != self.capacity; ++curr_idx) + { + if (self.data[curr_idx] == HASH_EMPTY) return NOT_FOUND~; + if (self.data[curr_idx] == search_hash && keys[curr_idx] == key) + { + return &values[curr_idx]; + } + } + + // if we searched from the start, no need to do wrap around search. + if (start_idx == 0) return NOT_FOUND~; + + curr_idx = 0; + + for (; curr_idx < start_idx; ++curr_idx) + { + if (self.data[curr_idx] == HASH_EMPTY) return NOT_FOUND~; + if (self.data[curr_idx] == search_hash && keys[curr_idx] == key) + { + return &values[curr_idx]; + } + } + + return NOT_FOUND~; +} + +<* + Get value associated with the provided key. +*> +fn Value? FlatMap.get(&self, Key key) +{ + if (try value_ref = self.get_ref(key)) + { + return *value_ref; + } + return NOT_FOUND~; +} + +<* + Checks if flat map has a value associated with the provided key. +*> +fn bool FlatMap.has(&self, Key key) +{ + if (catch self.get(key)) return false; + return true; +} + +<* + Removes key-value pair from a flat map if it exists. +*> +fn void? FlatMap.remove(&self, Key key) @maydiscard +{ + HashType search_hash = calc_hash(key); + sz start_idx = get_idx(search_hash, self.capacity); + sz curr_idx = start_idx; + Pair{Key[], Value[]} kv = keys_values_array(self.data, self.capacity); + Key[] keys = kv.first; + Value[] values = kv.second; + + for (; curr_idx < self.capacity; ++curr_idx) + { + if (self.data[curr_idx] == HASH_EMPTY) return NOT_FOUND~; + if (self.data[curr_idx] == search_hash && keys[curr_idx] == key) + { + self.data[curr_idx] = HASH_DELETED; + self.count--; + return; + } + } + + // if we searched from the start, no need to do wrap around search. + if (start_idx == 0) return NOT_FOUND~; + + curr_idx = 0; + + for (; curr_idx < start_idx; ++curr_idx) + { + if (self.data[curr_idx] == HASH_EMPTY) return NOT_FOUND~; + if (self.data[curr_idx] == search_hash && keys[curr_idx] == key) + { + self.data[curr_idx] = HASH_DELETED; + self.count--; + return; + } + } + + return NOT_FOUND~; +} + +<* + Iterates over each key-value pair in the map. + + @require self.is_initialized() : "Must be initialized map" +*> +macro void FlatMap.@each_pair(&self; @body(Key key, Value value)) +{ + if (@unlikely(self.is_empty())) return; + + HashType[] hashes = hashes_array(self); + Key[] keys = keys_array(self); + Value[] values = values_array(self); + + foreach (idx, hash : hashes) + { + if (hash >= HASH_FIRST_VALID) + { + @body(keys[idx], values[idx]); + } + } +} + +<* + Iterates over each key in the map. + + @require self.is_initialized() : "Must be initialized map" +*> +macro void FlatMap.@each_key(&self; @body(Key key)) +{ + if (@unlikely(self.is_empty())) return; + + HashType[] hashes = hashes_array(self); + Key[] keys = keys_array(self); + + foreach (idx, hash : hashes) + { + if (hash >= HASH_FIRST_VALID) + { + @body(keys[idx]); + } + } +} + +<* + Iterates over each value in the map. + + @require self.is_initialized() : "Must be initialized map" +*> +macro void FlatMap.@each_value(&self; @body(Value value)) +{ + if (@unlikely(self.is_empty())) return; + + HashType[] hashes = hashes_array(self); + Value[] values = values_array(self); + + foreach (idx, hash : hashes) + { + if (hash >= HASH_FIRST_VALID) + { + @body(values[idx]); + } + } +} + +fn void grow(FlatMap* self) @private +{ + HashType* old_data = self.data; + sz old_capacity = self.capacity; + sz new_capacity = max(old_capacity, DEFAULT_CAPACITY) * 2; + sz mem_req = calc_mem_req(new_capacity); + HashType* new_data = (HashType*)alloc::malloc(self.alloc, mem_req); + reset_hashes(new_data[:new_capacity]); + + sz old_idx = 0; + Pair{Key[], Value[]} kvs = keys_values_array(old_data, old_capacity); + Key[] old_keys = kvs.first; + Value[] old_values = kvs.second; + + for (; old_idx < old_capacity; ++old_idx) + { + if (old_data[old_idx] < HASH_FIRST_VALID) continue; + // Re-index and insert. + Key old_key = old_keys[old_idx]; + Value old_val = old_values[old_idx]; + HashType hash = old_data[old_idx]; + set_inner(new_data, new_capacity, null, hash, old_key, old_val); + } + + alloc::free(self.alloc, old_data); + self.data = new_data; + self.capacity = new_capacity; +} + +fn bool FlatMap.is_initialized(&self) @inline +{ + return self.alloc != null; +} + +fn bool FlatMap.is_empty(&self) @inline +{ + return self.count == 0; +} + +<* + Returns array of all keys. +*> +fn Key[] FlatMap.keys(&self, Allocator alloc) +{ + Key[] key_buff = alloc::alloc_array(alloc, Key, self.count); + sz key_buff_used = 0; + Key[] exist_keys = keys_array(self); + HashType[] hashes = hashes_array(self); + + foreach (idx, hash : hashes) + { + if (hash >= HASH_FIRST_VALID) + { + key_buff[key_buff_used++] = exist_keys[idx]; + } + } + + return key_buff; +} + +<* + Returns array of all keys. +*> +fn Key[] FlatMap.tkeys(&self) +{ + return self.keys(tmem); +} + +<* + Returns array of all values. +*> +fn Value[] FlatMap.values(&self, Allocator alloc) +{ + Value[] value_buff = alloc::alloc_array(alloc, Value, self.count); + sz value_buff_used = 0; + Value[] exist_values = values_array(self); + HashType[] hashes = hashes_array(self); + + foreach (idx, hash : hashes) + { + if (hash >= HASH_FIRST_VALID) + { + value_buff[value_buff_used++] = exist_values[idx]; + } + } + + return value_buff; +} + +<* + Returns array of all values. +*> +fn Value[] FlatMap.tvalues(&self) +{ + return self.values(tmem); +} + +fn void reset_hashes(HashType[] hashes) @private +{ + foreach (&hash : hashes) + { + *hash = HASH_EMPTY; + } +} + +fn bool crossed_threshold(FlatMap* self) @inline @private +{ + // Capacity is a power of 2; avoid float division on every insert. + return self.count >= (sz)(self.capacity * self.load_factor); +} + +fn sz calc_mem_req(sz want_items) @private @inline +{ + return HashType::size * want_items + Key::size * want_items + Value::size * want_items + Key::alignment + + Value::alignment; +} + +fn Pair{Key[], Value[]} keys_values_array(HashType* data, sz capacity) @private +{ + Pair{Key[], Value[]} res @noinit; + res.first = ((Key*)mem::aligned_pointer((Key*)(data + capacity), Key::alignment))[:capacity]; + res.second = ((Value*)mem::aligned_pointer((Value*)(res.first.ptr + capacity), Value::alignment))[:capacity]; + return res; +} + +fn HashType[] hashes_array(FlatMap* self) @private @inline +{ + return self.data[:self.capacity]; +} + +fn Key[] keys_array(FlatMap* self) @private +{ + return ((Key*)mem::aligned_pointer((Key*)(self.data + self.capacity), Key::alignment))[:self.capacity]; +} + +fn Value[] values_array(FlatMap* self) @private +{ + Key[] keys = keys_array(self); + return ((Value*)mem::aligned_pointer((Value*)(keys.ptr + self.capacity), Value::alignment))[:self.capacity]; +} + +fn HashType calc_hash(Key key) @inline +{ + HashType hash = key.hash(); + return max(hash, HASH_FIRST_VALID); +} + +<* + @require math::is_power_of_2(capacity) : "Must be a power of 2" +*> +fn sz get_idx(HashType hash, sz capacity) @inline +{ + HashType cap_u = (HashType)capacity; + return max((sz)(hash & (cap_u - 1u)) - (sz)HASH_FIRST_VALID, (sz)0); +} diff --git a/lib/std/collections/flatset.c3 b/lib/std/collections/flatset.c3 new file mode 100644 index 000000000..408565395 --- /dev/null +++ b/lib/std/collections/flatset.c3 @@ -0,0 +1,534 @@ +<* + @require types::has_equals(Value) : "Must have equality operator overloaded" + @require $defined((Value) { }.hash()) : "Must be hashable" +*> +module std::collections::flatset ; +import std::math; +import std::io; +import std::collections::pair; + +alias HashType = uint; +const sz DEFAULT_CAPACITY @private = 16; +const float DEFAULT_LOAD_FACTOR @private = 0.85f; +const HashType HASH_EMPTY @private = 0; +const HashType HASH_DELETED @private = 1; +const HashType HASH_FIRST_VALID @private = 2; + +<* + FlatSet is a hash set whose elements live together in one compact, contiguous + region, unlike separate chaining where each element is a node heap-allocated + individually and chained off its bucket. The flat storage is cache-friendly + and memory-lean, so membership tests, inserts and full iteration stay quick + under dense use, yet the data must be redistributed whenever the set outgrows + its capacity. Chaining is more tolerant of skewed hashes and makes deletion + cheap and predictable, but pays a per-element allocation and pointer-chasing + overhead that penalizes sequential and hot-path workloads. +*> +struct FlatSet +{ + HashType* data; + Allocator alloc; + sz count; + sz capacity; + float load_factor; +} + +<* + @param [&inout] allocator : "The allocator to use" + @require capacity > 0 : "The capacity must be 1 or higher" + @require load_factor > 0.0 && load_factor <= 1 : "The load factor must be higher than 0 and less or equal to 1" + @require !self.is_initialized() : "Set was already initialized" +*> +fn FlatSet* FlatSet.init( + &self, + Allocator allocator, + sz capacity = DEFAULT_CAPACITY, + float load_factor = DEFAULT_LOAD_FACTOR +) +{ + sz mem_req = calc_mem_req(math::next_power_of_2(capacity)); + self.data = alloc::malloc(allocator, mem_req); + self.capacity = capacity; + self.count = 0; + self.alloc = allocator; + self.load_factor = load_factor; + reset_hashes(self.data[:self.capacity]); + return self; +} + +<* + @require capacity > 0 : "The capacity must be 1 or higher" + @require load_factor > 0.0 && load_factor <= 1 : "The load factor must be higher than 0 and less or equal to 1" + @require !self.is_initialized() : "Set was already initialized" +*> +fn FlatSet* FlatSet.tinit(&self, sz capacity = DEFAULT_CAPACITY, float load_factor = DEFAULT_LOAD_FACTOR) +{ + return self.init(tmem, capacity, load_factor); +} + +<* + @param [in] values : "The values for the FlatSet entries" + @param [&inout] allocator : "The allocator to use" + @require capacity > 0 : "The capacity must be 1 or higher" + @require load_factor > 0.0 && load_factor <= 1 : "The load factor must be higher than 0 and less or equal to 1" + @require !self.is_initialized() : "Set was already initialized" +*> +fn FlatSet* FlatSet.init_from_values( + &self, + Allocator allocator, + Value[] values, + sz capacity = DEFAULT_CAPACITY, + float load_factor = DEFAULT_LOAD_FACTOR +) +{ + self.init(allocator, capacity, load_factor); + foreach (value : values) + { + self.add(value); + } + return self; +} + +<* + @param [in] values : "The values for the FlatSet entries" + @require capacity > 0 : "The capacity must be 1 or higher" + @require load_factor > 0.0 && load_factor <= 1 : "The load factor must be higher than 0 and less or equal to 1" + @require !self.is_initialized() : "Set was already initialized" +*> +fn FlatSet* FlatSet.tinit_from_values( + &self, + Value[] values, + sz capacity = DEFAULT_CAPACITY, + float load_factor = DEFAULT_LOAD_FACTOR +) +{ + return self.init_from_values(tmem, values, capacity, load_factor); +} + +<* + Frees all underlying resources. + Shouldn't be used after this operation without initialization. +*> +fn void FlatSet.free(&self) +{ + if (self.data) + { + alloc::free(self.alloc, self.data); + *self = { }; + } +} + +<* + Resets underlying data, so flat set can be reused. +*> +fn void FlatSet.clear(&self) +{ + self.count = 0; + HashType[] hashes = self.data[:self.capacity]; + reset_hashes(hashes); +} + +<* + Inserts a value into the flat set. +*> +fn bool FlatSet.add(&self, Value value) +{ + if (crossed_threshold(self)) grow(self); + HashType hash = calc_hash(value); + return add_inner(self.data, self.capacity, &self.count, hash, value); +} + +fn bool add_inner(HashType* data, sz capacity, sz* count, HashType hash, Value value) @private @inline +{ + sz start_idx = get_idx(hash, capacity); + sz curr_idx = start_idx; + Value[] values = values_array(data, capacity); + sz insert_slot_idx = -1; + + for (; curr_idx < capacity; ++curr_idx) + { + // We need to try to find closest slot possible, but we can't break from here. + // We need to iterate over all probe sequence to scan for duplicates. + if (insert_slot_idx == -1 && data[curr_idx] < HASH_FIRST_VALID) + { + insert_slot_idx = curr_idx; + continue; + } + // Element was already in the set. + if (hash == data[curr_idx] && values[curr_idx] == value) return false; + } + + if (insert_slot_idx != -1) + { + data[insert_slot_idx] = hash; + values[insert_slot_idx] = value; + if (count) (*count)++; + return true; + } + + // Wrapped around, start from first entry. + curr_idx = 0; + + for (; curr_idx < start_idx; ++curr_idx) + { + // We need to try to find closest slot possible, but we can't break from here. + // We need to iterate over all probe sequence to scan for duplicates. + if (insert_slot_idx == -1 && data[curr_idx] < HASH_FIRST_VALID) + { + insert_slot_idx = curr_idx; + continue; + } + // Element was already in the set. + if (hash == data[curr_idx] && values[curr_idx] == value) return false; + } + + if (@unlikely(insert_slot_idx == -1)) + { + unreachable("Failed to insert a value into a hashset"); + } + + data[insert_slot_idx] = hash; + values[insert_slot_idx] = value; + if (count) (*count)++; + return true; +} + +<* + Copies all elements from the input set into the current set. + + @require self.is_initialized() : "Must be initialized set" + @require other.is_initialized() : "Must be initialized set" +*> +fn void FlatSet.add_all_from(&self, FlatSet* other) +{ + if (@unlikely(other.is_empty())) return; + + HashType[] other_hashes = hashes_array(other); + Value[] other_values = values_array(other.data, other.capacity); + foreach (idx, hash : other_hashes) + { + if (hash >= HASH_FIRST_VALID) + { + self.add(other_values[idx]); + } + } +} + +<* + Checks if flat set has the provided value. +*> +fn bool FlatSet.contains(&self, Value value) +{ + HashType search_hash = calc_hash(value); + sz start_idx = get_idx(search_hash, self.capacity); + sz curr_idx = start_idx; + Value[] values = values_array(self.data, self.capacity); + + for (; curr_idx != self.capacity; ++curr_idx) + { + if (self.data[curr_idx] == HASH_EMPTY) return false; + if (self.data[curr_idx] == search_hash && values[curr_idx] == value) + { + return true; + } + } + + // if we searched from the start, no need to do wrap around search. + if (start_idx == 0) return false; + + curr_idx = 0; + + for (; curr_idx < start_idx; ++curr_idx) + { + if (self.data[curr_idx] == HASH_EMPTY) return false; + if (self.data[curr_idx] == search_hash && values[curr_idx] == value) + { + return true; + } + } + + return false; +} + +<* + Removes the value from a flat set if it exists. +*> +fn bool FlatSet.remove(&self, Value value) +{ + HashType search_hash = calc_hash(value); + sz start_idx = get_idx(search_hash, self.capacity); + sz curr_idx = start_idx; + Value[] values = values_array(self.data, self.capacity); + + for (; curr_idx < self.capacity; ++curr_idx) + { + if (self.data[curr_idx] == HASH_EMPTY) return false; + if (self.data[curr_idx] == search_hash && values[curr_idx] == value) + { + self.data[curr_idx] = HASH_DELETED; + self.count--; + return true; + } + } + + // if we searched from the start, no need to do wrap around search. + if (start_idx == 0) return false; + + curr_idx = 0; + + for (; curr_idx < start_idx; ++curr_idx) + { + if (self.data[curr_idx] == HASH_EMPTY) return false; + if (self.data[curr_idx] == search_hash && values[curr_idx] == value) + { + self.data[curr_idx] = HASH_DELETED; + self.count--; + return true; + } + } + + return false; +} + +// Set operations. + +<* + Returns the union of two sets (A | B) + + @param [&in] other : "The other set to union with" + @param [&inout] allocator : "Allocator for the new set" + @return "A new set containing the union of both sets" +*> +fn FlatSet FlatSet.set_union(&self, Allocator allocator, FlatSet* other) +{ + sz new_capacity = math::next_power_of_2(self.count + other.count); + FlatSet result; + result.init(allocator, self.capacity + other.capacity, self.load_factor); + result.add_all_from(self); + result.add_all_from(other); + return result; +} + +fn FlatSet FlatSet.tset_union(&self, FlatSet* other) => self.set_union(tmem, other); + +<* + Returns the intersection of the two sets (A & B) + + @param [&in] other : "The other set to intersect with" + @param [&inout] allocator : "Allocator for the new set" + @return "A new set containing the intersection of both sets" +*> +fn FlatSet FlatSet.intersection(&self, Allocator allocator, FlatSet* other) +{ + FlatSet result; + result.init(allocator, math::min(self.capacity, other.capacity), self.load_factor); + + // Iterate through the smaller set for efficiency + FlatSet* smaller = self.count <= other.count ? self : other; + FlatSet* larger = self.count > other.count ? self : other; + + smaller.@each(; Value value) + { + if (larger.contains(value)) result.add(value); + }; + + return result; +} + +fn FlatSet FlatSet.tintersection(&self, FlatSet* other) => self.intersection(tmem, other); + +<* + Return this set - other, so (A & ~B) + + @param [&in] other : "The other set to compare with" + @param [&inout] allocator : "Allocator for the new set" + @return "A new set containing elements in this set but not in the other" +*> +fn FlatSet FlatSet.difference(&self, Allocator allocator, FlatSet* other) +{ + FlatSet result; + result.init(allocator, self.capacity, self.load_factor); + self.@each(; Value value) + { + if (!other.contains(value)) + { + (void)result.add(value); + } + }; + return result; +} + +fn FlatSet FlatSet.tdifference(&self, FlatSet* other) => self.difference(tmem, other) @inline; + +<* + Return (A ^ B) + + @param [&in] other : "The other set to compare with" + @param [&inout] allocator : "Allocator for the new set" + @return "A new set containing elements in this set or the other, but not both" +*> +fn FlatSet FlatSet.symmetric_difference(&self, Allocator allocator, FlatSet* other) +{ + FlatSet result; + result.init(allocator, self.capacity, self.load_factor); + result.add_all_from(self); + other.@each(; Value value) + { + if (!result.add(value)) + { + (void)result.remove(value); + } + }; + return result; +} + +fn FlatSet FlatSet.tsymmetric_difference(&self, FlatSet* other) => self.symmetric_difference(tmem, other) @inline; + +<* + Check if this hash set is a subset of another set. + + @param [&in] other : "The other set to check against" + @return "True if all elements of this set are in the other set" +*> +fn bool FlatSet.is_subset(&self, FlatSet* other) +{ + if (self.count == 0) return true; + if (self.count > other.count) return false; + + self.@each(; Value value) + { + if (!other.contains(value)) return false; + }; + return true; +} + +<* + Returns array of all values. +*> +fn Value[] FlatSet.values(&self, Allocator alloc) +{ + Value[] value_buff = alloc::alloc_array(alloc, Value, self.count); + sz value_buff_used = 0; + HashType[] hashes = hashes_array(self); + Value[] exist_values = values_array(self.data, self.capacity); + + foreach (idx, hash : hashes) + { + if (hash >= HASH_FIRST_VALID) + { + value_buff[value_buff_used++] = exist_values[idx]; + } + } + + return value_buff; +} + +<* + Returns array of all values. +*> +fn Value[] FlatSet.tvalues(&self) +{ + return self.values(tmem); +} + +<* + Iterates over each inserted value in the set. + + @require self.is_initialized() : "Must be initialized set" +*> +macro void FlatSet.@each(&self; @body(Value value)) +{ + if (@unlikely(self.is_empty())) return; + + HashType[] hashes = hashes_array(self); + Value[] values = values_array(self.data, self.capacity); + + foreach (idx, hash : hashes) + { + if (hash >= HASH_FIRST_VALID) + { + @body(values[idx]); + } + } +} + +fn void grow(FlatSet* self) @private +{ + HashType* old_data = self.data; + sz old_capacity = self.capacity; + sz new_capacity = max(old_capacity, DEFAULT_CAPACITY) * 2; + sz mem_req = calc_mem_req(new_capacity); + HashType* new_data = (HashType*)alloc::malloc(self.alloc, mem_req); + reset_hashes(new_data[:new_capacity]); + + sz old_idx = 0; + Value[] old_values = values_array(old_data, old_capacity); + + for (; old_idx < old_capacity; ++old_idx) + { + if (old_data[old_idx] < HASH_FIRST_VALID) continue; + // Re-index and insert. + Value old_val = old_values[old_idx]; + HashType hash = old_data[old_idx]; + add_inner(new_data, new_capacity, null, hash, old_val); + } + + alloc::free(self.alloc, old_data); + self.data = new_data; + self.capacity = new_capacity; +} + +fn bool FlatSet.is_initialized(&self) @inline +{ + return self.alloc != null; +} + +fn bool FlatSet.is_empty(&self) @inline +{ + return self.count == 0; +} + +fn void reset_hashes(HashType[] hashes) @private +{ + foreach (&hash : hashes) + { + *hash = HASH_EMPTY; + } +} + +fn bool crossed_threshold(FlatSet* self) @private @inline +{ + // Capacity is a power of 2; avoid float division on every insert. + return self.count >= (sz)(self.capacity * self.load_factor); +} + +fn sz calc_mem_req(sz want_items) @private @inline +{ + return HashType::size * want_items + Value::size * want_items + Value::alignment; +} + +fn HashType[] hashes_array(FlatSet* self) @private +{ + return self.data[:self.capacity]; +} + +fn Value[] values_array(HashType* data, sz capacity) @private +{ + Value* ptr = mem::aligned_pointer((Value*)(data + capacity), Value::alignment); + return ptr[:capacity]; +} + +fn HashType calc_hash(Value val) @inline +{ + HashType hash = val.hash(); + return max(hash, HASH_FIRST_VALID); +} + +<* + @require math::is_power_of_2(capacity) : "Must be a power of 2" +*> +fn sz get_idx(HashType hash, sz capacity) @inline +{ + HashType cap_u = (HashType)capacity; + return max((sz)(hash & (cap_u - 1u)) - (sz)HASH_FIRST_VALID, (sz)0); +} diff --git a/releasenotes.md b/releasenotes.md index f46da52e7..34b9a9f72 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -40,6 +40,7 @@ - Add `log::get_logger`. - `RefCounted` now correctly makes a difference between dealloc and free. - `Path.is_link` added. +- `FlatMap` and `FlatSet` added. ### Fixes - Vmem incorrectly handled reserve page sizes. diff --git a/test/unit/stdlib/collections/flatmap.c3 b/test/unit/stdlib/collections/flatmap.c3 new file mode 100644 index 000000000..968dc556f --- /dev/null +++ b/test/unit/stdlib/collections/flatmap.c3 @@ -0,0 +1,290 @@ +module flatmap_test @test; +import std::collections::flatmap; +import std::collections::list; + +fn void test_with_strings() +{ + List{String} keys; + keys.init(tmem, 100); + defer keys.free(); + + for (sz i = 0; i < 100; ++i) + { + keys.push(gen_random_string(tmem, 20)); + } + + FlatMap{String, sz} map; + map.init(tmem, 100); + + foreach (idx, key : keys) + { + map.set(key, idx); + } + + foreach (idx, key : keys) + { + test::eq(map.get(key)!!, idx); + } +} + +fn void test_init_tinit() +{ + FlatMap{sz, sz} map; + map.init(tmem, 32); + map.set(1, 10); + test::eq(map.get(1)!!, (sz)10); + + FlatMap{sz, sz} tmap; + tmap.tinit(); + tmap.set(5, 50); + test::eq(tmap.get(5)!!, (sz)50); + + FlatMap{sz, sz} hmap; + hmap.init(tmem, 16, 0.5f); + hmap.set(9, 90); + test::eq(hmap.get(9)!!, (sz)90); +} + +fn void test_init_from_keys_and_values() +{ + FlatMap{sz, sz} map; + map.init_from_keys_and_values(tmem, { 10, 20, 30 }, { 1, 2, 3 }); + test::eq(map.get(10)!!, (sz)1); + test::eq(map.get(20)!!, (sz)2); + test::eq(map.get(30)!!, (sz)3); + test::eq(map.has(40), false); + + FlatMap{sz, sz} tmap; + tmap.tinit_from_keys_and_values({ 1, 2, 3 }, { 4, 5, 6 }); + test::eq(tmap.get(1)!!, (sz)4); + test::eq(tmap.get(3)!!, (sz)6); + + // Duplicate keys are overwritten (last wins), count reflects unique keys. + FlatMap{sz, sz} dmap; + dmap.tinit_from_keys_and_values({ 1, 1, 2 }, { 10, 20, 30 }); + test::eq(dmap.get(1)!!, (sz)20); + test::eq(dmap.get(2)!!, (sz)30); + test::eq(dmap.keys(tmem).len, (sz)2); +} + +fn void test_set_get() +{ + FlatMap{sz, sz} map; + map.tinit(); + + // Enough insertions to trigger grow() several times (default capacity 16). + for (sz i = 0; i < 100; ++i) + { + map.set(i, i * 10); + } + for (sz i = 0; i < 100; ++i) + { + test::eq(map.get(i)!!, i * 10); + } + + // Overwriting an existing key does not add a new entry. + map.set(0, 42); + test::eq(map.get(0)!!, (sz)42); + + // Getting a missing key yields NOT_FOUND. + test::@error(map.get(10000)); +} + +fn void test_get_ref() +{ + FlatMap{sz, sz} map; + map.tinit(); + map.set(7, 100); + + sz* v = map.get_ref(7)!!; + test::eq(*v, (sz)100); + + // Mutating through the returned reference changes the stored value. + *v = 999; + test::eq(map.get(7)!!, (sz)999); + + test::@error(map.get_ref(42)); +} + +fn void test_has() +{ + FlatMap{sz, sz} map; + map.tinit(); + map.set(1, 10); + test::eq(map.has(1), true); + test::eq(map.has(2), false); +} + +fn void test_remove() +{ + FlatMap{sz, sz} map; + map.tinit(); + map.set(1, 10); + map.set(2, 20); + + // Removing a present key succeeds. + if (catch map.remove(1)) return; + test::eq(map.has(1), false); + + // Removing the same key again yields NOT_FOUND. + test::@error(map.remove(1)); + + // Re-setting after removal reuses the deleted slot. + map.set(1, 99); + test::eq(map.get(1)!!, (sz)99); +} + +fn void test_clear() +{ + FlatMap{sz, sz} map; + map.tinit(); + map.set(1, 10); + map.set(2, 20); + + map.clear(); + test::@error(map.get(1)); + test::eq(map.has(1), false); + test::eq(map.has(2), false); + + // Map remains usable after clear. + map.set(3, 30); + test::eq(map.get(3)!!, (sz)30); +} + +fn void test_free() +{ + FlatMap{sz, sz} map; + map.init(tmem, 32); + map.set(1, 10); + test::eq(map.get(1)!!, (sz)10); + map.free(); + + // Map can be re-initialized and reused after free. + map.init(tmem, 16); + map.set(2, 20); + test::eq(map.get(2)!!, (sz)20); +} + +fn void test_keys_values() +{ + FlatMap{sz, sz} map; + map.tinit(); + for (sz i = 0; i < 10; ++i) + { + map.set(i, i * 2); + } + map.remove(5); + + sz[] keys = map.keys(tmem); + sz[] tkeys = map.tkeys(); + sz[] values = map.values(tmem); + sz[] tvalues = map.tvalues(); + + test::eq(keys.len, (sz)9); + test::eq(tkeys.len, (sz)9); + test::eq(values.len, (sz)9); + test::eq(tvalues.len, (sz)9); + + // Order is unspecified, so check sums. + sz key_sum = 0; + foreach (k : keys) key_sum += k; + test::eq(key_sum, (sz)40); + + sz value_sum = 0; + foreach (v : values) value_sum += v; + test::eq(value_sum, (sz)80); +} + +fn void test_each_pair() +{ + FlatMap{sz, sz} map; + map.tinit(); + for (sz i = 0; i < 10; ++i) + { + map.set(i, i * 3); + } + + sz count = 0; + sz sum = 0; + map.@each_pair(; sz key, sz value) + { + count++; + sum += value; + test::eq(value, key * 3); + }; + test::eq(count, (sz)10); + test::eq(sum, (sz)135); +} + +fn void test_each_key() +{ + FlatMap{sz, sz} map; + map.tinit(); + for (sz i = 0; i < 10; ++i) + { + map.set(i, i * 3); + } + + sz count = 0; + sz sum = 0; + map.@each_key(; sz key) + { + count++; + sum += key; + }; + test::eq(count, (sz)10); + test::eq(sum, (sz)45); +} + +fn void test_each_value() +{ + FlatMap{sz, sz} map; + map.tinit(); + for (sz i = 0; i < 10; ++i) + { + map.set(i, i * 3); + } + + sz count = 0; + sz sum = 0; + map.@each_value(; sz value) + { + count++; + sum += value; + }; + test::eq(count, (sz)10); + test::eq(sum, (sz)135); +} + +fn void test_string_keys() +{ + FlatMap{String, sz} map; + map.tinit(); + String[] words = { "apple", "banana", "cherry", "date" }; + for (sz i = 0; i < 4; ++i) + { + map.set(words[i], i); + } + + test::eq(map.get("apple")!!, (sz)0); + test::eq(map.get("cherry")!!, (sz)2); + + if (catch map.remove("banana")) return; + test::eq(map.has("banana"), false); + + sz[] vals = map.tvalues(); + test::eq(vals.len, (sz)3); +} + +module flatmap_test; +import std::math::random; + +fn String gen_random_string(Allocator alloc, sz len) +{ + String res = (String)alloc::alloc_array(alloc, char, len); + foreach (&c : res) + { + *c = (char)(random::rand('z' - 'a') + 'a'); + } + return res; +} diff --git a/test/unit/stdlib/collections/flatset.c3 b/test/unit/stdlib/collections/flatset.c3 new file mode 100644 index 000000000..9a4f42ebe --- /dev/null +++ b/test/unit/stdlib/collections/flatset.c3 @@ -0,0 +1,271 @@ +module flatset_test @test; +import std::collections::flatset; + +fn void test_init_tinit() +{ + FlatSet{sz} set; + set.init(tmem, 32); + set.add(1); + set.add(2); + test::eq(set.contains(1), true); + test::eq(set.contains(2), true); + + FlatSet{sz} tset; + tset.tinit(); + tset.add(3); + test::eq(tset.contains(3), true); + test::eq(tset.contains(4), false); + + FlatSet{sz} hset; + hset.init(tmem, 16, 0.5f); + hset.add(5); + test::eq(hset.contains(5), true); +} + +fn void test_init_from_values() +{ + FlatSet{sz} set; + set.init_from_values(tmem, { 1, 2, 2, 3, 3, 3 }); + test::eq(set.contains(1), true); + test::eq(set.contains(2), true); + test::eq(set.contains(3), true); + test::eq(set.contains(4), false); + test::eq(set.tvalues().len, (sz)3); + + FlatSet{sz} tset; + tset.tinit_from_values({ 5, 5, 6 }); + test::eq(tset.contains(5), true); + test::eq(tset.contains(6), true); + test::eq(tset.tvalues().len, (sz)2); +} + +fn void test_add_contains() +{ + FlatSet{sz} set; + set.tinit(); + + test::eq(set.add(1), true); + test::eq(set.add(2), true); + // Duplicate insert reports false. + test::eq(set.add(1), false); + + test::eq(set.contains(1), true); + test::eq(set.contains(2), true); + test::eq(set.contains(3), false); + + // Enough insertions to trigger grow() several times (default capacity 16). + for (sz i = 0; i < 100; ++i) + { + if (!set.add(i + 10)) return; + } + for (sz i = 0; i < 100; ++i) + { + test::eq(set.contains(i + 10), true); + } + test::eq(set.contains(9999), false); +} + +fn void test_remove() +{ + FlatSet{sz} set; + set.tinit(); + set.add(1); + set.add(2); + + test::eq(set.remove(1), true); + test::eq(set.contains(1), false); + + // Removing an absent value reports false. + test::eq(set.remove(1), false); + + // Re-adding after removal reuses the deleted slot. + set.add(1); + test::eq(set.contains(1), true); + test::eq(set.contains(2), true); +} + +fn void test_add_all_from() +{ + FlatSet{sz} a; + a.tinit(); + a.add(1); + a.add(2); + + FlatSet{sz} b; + b.tinit(); + b.add(2); + b.add(3); + + a.add_all_from(&b); + test::eq(a.contains(1), true); + test::eq(a.contains(2), true); + test::eq(a.contains(3), true); + test::eq(a.tvalues().len, (sz)3); +} + +fn void test_clear() +{ + FlatSet{sz} set; + set.tinit(); + set.add(1); + set.add(2); + + set.clear(); + test::eq(set.contains(1), false); + test::eq(set.contains(2), false); + test::eq(set.tvalues().len, (sz)0); + + // Set remains usable after clear. + set.add(3); + test::eq(set.contains(3), true); +} + +fn void test_free() +{ + FlatSet{sz} set; + set.init(tmem, 32); + set.add(1); + test::eq(set.contains(1), true); + set.free(); + + // Set can be re-initialized and reused after free. + set.init(tmem, 16); + set.add(2); + test::eq(set.contains(2), true); +} + +fn void test_set_union() +{ + FlatSet{sz} a = range_set(0, 10); + FlatSet{sz} b = range_set(5, 15); + + FlatSet{sz} res = a.set_union(tmem, &b); + defer res.free(); + test::eq(res.contains(0), true); + test::eq(res.contains(5), true); + test::eq(res.contains(14), true); + test::eq(res.contains(15), false); + test::eq(res.tvalues().len, (sz)15); + + FlatSet{sz} tres = a.tset_union(&b); + defer tres.free(); + test::eq(tres.contains(14), true); + test::eq(tres.contains(15), false); +} + +fn void test_intersection() +{ + FlatSet{sz} a = range_set(0, 10); + FlatSet{sz} b = range_set(5, 15); + + FlatSet{sz} res = a.intersection(tmem, &b); + defer res.free(); + test::eq(res.contains(5), true); + test::eq(res.contains(9), true); + test::eq(res.contains(4), false); + test::eq(res.contains(10), false); + test::eq(res.tvalues().len, (sz)5); + + FlatSet{sz} tres = a.tintersection(&b); + defer tres.free(); + test::eq(tres.contains(9), true); + test::eq(tres.contains(10), false); +} + +fn void test_difference() +{ + FlatSet{sz} a = range_set(0, 10); + FlatSet{sz} b = range_set(5, 15); + + FlatSet{sz} res = a.difference(tmem, &b); + defer res.free(); + test::eq(res.contains(0), true); + test::eq(res.contains(4), true); + test::eq(res.contains(5), false); + test::eq(res.contains(9), false); + test::eq(res.tvalues().len, (sz)5); +} + +fn void test_symmetric_difference() +{ + FlatSet{sz} a = range_set(0, 10); + FlatSet{sz} b = range_set(5, 15); + + FlatSet{sz} res = a.symmetric_difference(tmem, &b); + defer res.free(); + test::eq(res.contains(0), true); + test::eq(res.contains(4), true); + test::eq(res.contains(5), false); + test::eq(res.contains(9), false); + test::eq(res.contains(10), true); + test::eq(res.contains(14), true); + test::eq(res.tvalues().len, (sz)10); +} + +fn void test_is_subset() +{ + FlatSet{sz} a = range_set(0, 5); + FlatSet{sz} b = range_set(0, 10); + FlatSet{sz} c = range_set(5, 10); + + test::eq(a.is_subset(&b), true); + test::eq(c.is_subset(&b), true); + test::eq(b.is_subset(&a), false); + + // A set is a subset of itself. + test::eq(a.is_subset(&a), true); + + // The empty set is a subset of any set. + FlatSet{sz} empty; + empty.tinit(); + test::eq(empty.is_subset(&b), true); + + // Disjoint sets are not subsets. + test::eq(c.is_subset(&a), false); +} + +fn void test_values() +{ + FlatSet{sz} set = range_set(0, 8); + bool removed = set.remove(3); + test::eq(removed, true); + + sz[] values = set.values(tmem); + sz[] tvalues = set.tvalues(); + test::eq(values.len, (sz)7); + test::eq(tvalues.len, (sz)7); + + // Sum of 0..7 (28) minus 3 = 25. Order is unspecified. + sz sum = 0; + foreach (v : values) sum += v; + test::eq(sum, (sz)25); +} + +fn void test_each() +{ + FlatSet{sz} set = range_set(0, 10); + + sz count = 0; + sz sum = 0; + set.@each(; sz value) + { + count++; + sum += value; + }; + test::eq(count, (sz)10); + test::eq(sum, (sz)45); +} + +module flatset_test; +import std::collections::flatset; + +fn FlatSet{sz} range_set(sz start, sz end) +{ + FlatSet{sz} set; + set.tinit(); + for (sz i = start; i < end; ++i) + { + set.add(i); + } + return set; +}