diff --git a/std/arraylist/arraylist.test.bro b/std/arraylist/arraylist.test.bro index 8a9393a..ae64e8d 100644 --- a/std/arraylist/arraylist.test.bro +++ b/std/arraylist/arraylist.test.bro @@ -7,7 +7,6 @@ handles_append test { try append(&list, 42) - try testing.expect_type(usize, list.items.len) try testing.expect_equal(1, list.items.len) try testing.expect_equal(42, list.items[0]) } diff --git a/std/build/build.bro b/std/build/build.bro index cf2f884..8e48122 100644 --- a/std/build/build.bro +++ b/std/build/build.bro @@ -7,11 +7,11 @@ # Declarative and literal-only: one executable per build. List fields take an # address-of an array literal (`&["raylib"]`) and default to empty. BuildConfig :: struct { - name []u8 # output executable name under root/build - source []u8 # program package directory, relative to build.bro + name []u8 # output executable name under root/build + source []u8 # program package directory, relative to build.bro libraries [][]u8 = &[] # library names to link (-l) lib_paths [][]u8 = &[] # library search directories (-L) - includes [][]u8 = &[] # C include directories (-I) - defines [][]u8 = &[] # C preprocessor defines (name or name=value) - links [][]u8 = &[] # extra linker inputs (object/source files, -framework pairs) + includes [][]u8 = &[] # C include directories (-I) + defines [][]u8 = &[] # C preprocessor defines (name or name=value) + links [][]u8 = &[] # extra linker inputs (object/source files, -framework pairs) } diff --git a/std/enums/enums.bro b/std/enums/enums.bro new file mode 100644 index 0000000..d6a2624 --- /dev/null +++ b/std/enums/enums.bro @@ -0,0 +1,45 @@ +import "@std/meta" + +EnumMap func($E, $V type) type { + match typeinfo!(E) { + .enum |info|: return struct { + present [info.fields.len]mut bool # fixme: replace with bitset + values [info.fields.len]mut V + } + else: compile_error!("EnumMap key must be an enum") + } +} + +init func( + $E, $V type, + values meta.EnumFieldStruct(E, ?V, some!(null)), +) EnumMap(E, V) { + map EnumMap(E, V) = undefined + + match typeinfo!(E) { + .enum |info|: expand for info.fields |field, i| { + map.present[i] = false + + if field!(values, field.name) |value| { + map.present[i] = true + map.values[i] = value + } + } + else: compile_error!("EnumMap key must be an enum") + } + + return map +} + +get func($E, $V type, map @EnumMap(E, V), key E) ?V { + # fixme: linear lookup; implement an enum index/discriminant map for O(1) lookup + match typeinfo!(E) { + .enum |info|: expand for info.fields |field, i| { + if key == field!(E, field.name) { + if (map.present[i]) return map.values[i] + return null + } + } + else: compile_error!("EnumMap key must be an enum") + } +} diff --git a/std/enums/enums.test.bro b/std/enums/enums.test.bro new file mode 100644 index 0000000..d50441a --- /dev/null +++ b/std/enums/enums.test.bro @@ -0,0 +1,25 @@ +import "@std/mem" +import "@std/testing" + +TestEnum :: enum(u8) { + ident = 3 + int = 8 + eof = 21 +} + +handles_sparse_enum_get test { + names EnumMap(TestEnum, []u8) = init({ + ident = "identifier", + int = "integer", + }) + + ident :: get(&names, TestEnum.ident) + + try testing.expect_type(?[]u8, ident) + try testing.expect(mem.eql("identifier", ident?)) + + eof :: get(&names, TestEnum.eof) + + try testing.expect_type(?[]u8, eof) + try testing.expect_equal(null, eof) +} diff --git a/std/hashmap/hashmap.bro b/std/hashmap/hashmap.bro new file mode 100644 index 0000000..6141049 --- /dev/null +++ b/std/hashmap/hashmap.bro @@ -0,0 +1,160 @@ +import "@std/mem" + +PutError :: enum { key_exists } + +Entry func($K, $V type) type { + return struct { + # hash = 0 means empty + hash usize = 0 + key K + value V + } +} + +HashMap func( + $K, $V type, + $hash_key func(key K) usize, + $keys_eql func(a, b K) bool, +) type { + return struct { + entries []mut Entry(K, V) + count usize + allocator mem.Allocator + } +} + +StringHashMap func($V type) type { + return HashMap([]u8, V, str_hash, str_eql) +} + +init func( + $K, $V type, + $hash_key func(key K) usize, + $keys_eql func(a, b K) bool, + allocator mem.Allocator, +) HashMap(K, V, hash_key, keys_eql) { + return HashMap(K, V, hash_key, keys_eql){ + entries = mem.empty(Entry(K, V)), + count = 0, + allocator = allocator, + } +} + +# free the entries in the hash map. +# note: this operation invalidates the map. +deinit func( + $K, $V type, + $hash_key func(key K) usize, + $keys_eql func(a, b K) bool, + map @HashMap(K, V, hash_key, keys_eql), +) void { mem.free(map.allocator, map.entries) } + +get func( + $K, $V type, + $hash_key func(key K) usize, + $keys_eql func(a, b K) bool, + map @HashMap(K, V, hash_key, keys_eql), + key K, +) ?V { + if (map.count == 0) return null + + hash :: normalize(hash_key(key)) + idx usize = hash & (map.entries.len - 1) + + while true { + entry :: map.entries[idx] + if (entry.hash == 0) return null + + if (entry.hash == hash and keys_eql(entry.key, key)) { + return entry.value + } + + idx = (idx + 1) & (map.entries.len - 1) + } +} + +put func( + $K, $V type, + $hash_key func(key K) usize, + $keys_eql func(a, b K) bool, + map @mut HashMap(K, V, hash_key, keys_eql), + key K, + value V, +) void ! (PutError | mem.AllocError) { + # check grow + threshold :: map.entries.len - divtrunc!(map.entries.len, 4) + if (map.entries.len == 0 or map.count + 1 > threshold) { + old_entries :: map.entries + new_size :: if (old_entries.len > 0) old_entries.len * 2 else 8 + new_entries :: try mem.alloc(Entry(K, V), map.allocator, new_size) + + # zero new entries + for 0..new_entries.len |i| { + new_entries[i].hash = 0 + } + + # move old entries + for old_entries |entry| { + if (entry.hash == 0) continue + + # find an empty slot + idx usize = entry.hash & (new_entries.len - 1) + while new_entries[idx].hash != 0 { + idx = (idx + 1) & (new_entries.len - 1) + } + + new_entries[idx] = entry + } + + map.entries = new_entries + mem.free(map.allocator, old_entries) + } + + # put new entry + hash :: normalize(hash_key(key)) + idx usize = hash & (map.entries.len - 1) + + while true { + entry :: map.entries[idx] + if entry.hash == 0 { + map.entries[idx] = Entry(K, V){ + hash = hash, + key = key, + value = value, + } + map.count += 1 + return + } + + if (entry.hash == hash and keys_eql(entry.key, key)) { + return .key_exists + } + + idx = (idx + 1) & (map.entries.len - 1) + } +} + +hide normalize func(hash usize) usize { + # mapping both 0 and 1 to 1 is safe because equality resolves + # collisions (since hash and key must both be equal). + if (hash == 0) return 1 + return hash +} + +#! FNV-1a hash implementation. +#! note: vulnerable to collision attacks. +hide str_hash func(key []u8) usize { + hash u32 = 2166136261 # offset basis + prime u32 = 16777619 + + for key |byte| { + product u64 :: u64(hash xor u32(byte)) * prime + hash = u32(product & u64(maxval!(u32))) + } + + return usize(hash) +} + +hide str_eql func(a, b []u8) bool { + return mem.eql(a, b) +} diff --git a/std/hashmap/hashmap.test.bro b/std/hashmap/hashmap.test.bro new file mode 100644 index 0000000..57ff8a4 --- /dev/null +++ b/std/hashmap/hashmap.test.bro @@ -0,0 +1,13 @@ +import "@std/mem" +import "@std/testing" + +handles_put_and_get test { + map StringHashMap(u32) = init(mem.c_allocator) + defer deinit(&map) + + try put(&map, "key", 42) + value :: get(&map, "key") + + try testing.expect_type(?u32, value) + try testing.expect_equal(42, value?) +} diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 1807bf8..1484a8f 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -41,17 +41,7 @@ eql func($T type, left, right []T) bool { return true } -hide empty_storage [1]mut u64 = [0] - -hide empty_slice func($T type, count usize) []mut T { - pointer *mut T :: ptrcast!(T, (&empty_storage).ptr) - return pointer[..count] -} - -empty func($T type) []mut T { - return empty_slice(T, 0) -} - +# allocate memory for a slice of type `T` with `count` elements. alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError { if count == 0 { return empty_slice(T, 0) @@ -73,10 +63,14 @@ alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError { return .out_of_memory } +# reallocate memory for a slice of type `T` with `new_count` elements. +# reallocating with `new_count == 0` will free the memory and return an empty slice. +# note: memory must be reallocated with the same allocator that was used to allocate it. realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mut T ! AllocError { if new_count == memory.len { return memory } + if new_count == 0 { free(allocator, memory) return empty_slice(T, 0) @@ -107,9 +101,12 @@ realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mu pointer *mut T :: ptrcast!(T, bytes) return pointer[..new_count] } + return .out_of_memory } +# free memory allocated for a slice of type `T`. +# note: memory must be freed with the same allocator that was used to allocate it. free func($T type, allocator Allocator, memory []T) void { if memory.len != 0 and sizeof!(T) != 0 { mutable_memory []mut T :: constcast!(memory) @@ -117,6 +114,19 @@ free func($T type, allocator Allocator, memory []T) void { } } +# get an empty slice of type `T` with `count` elements. +empty_slice func($T type, count usize) []mut T { + pointer *mut T :: ptrcast!(T, (&empty_storage).ptr) + return pointer[..count] +} + +# get an empty slice of type `T` with 0 elements. +empty func($T type) []mut T { + return empty_slice(T, 0) +} + +hide empty_storage [1]mut u64 = [0] + hide malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption. hide power_of_two func(value usize) bool { diff --git a/std/meta/meta.bro b/std/meta/meta.bro index e9b0dc0..5780e89 100644 --- a/std/meta/meta.bro +++ b/std/meta/meta.bro @@ -44,9 +44,9 @@ TypeInfo :: union(enum) { EnumFieldStruct func($E, $Field type, $default ?Field) type { match typeinfo!(E) { .enum |info|: { - names [info.fields.len]mut []u8 = undefined - field_types [info.fields.len]mut type = undefined - defaults [info.fields.len]mut ?Field = undefined + names [info.fields.len]mut []u8 = undefined + field_types [info.fields.len]mut type = undefined + defaults [info.fields.len]mut ?Field = undefined expand for info.fields |field, index| { names[index] = field.name field_types[index] = Field diff --git a/std/std.bro b/std/std.bro index d4697ad..0da939b 100644 --- a/std/std.bro +++ b/std/std.bro @@ -1,5 +1,7 @@ import "io" +import "enums" import "arraylist" Io :: alias io.Io ArrayList :: alias arraylist.ArrayList +EnumMap :: alias enums.EnumMap diff --git a/std/testing/testing.bro b/std/testing/testing.bro index e5e08c0..964dab3 100644 --- a/std/testing/testing.bro +++ b/std/testing/testing.bro @@ -38,9 +38,11 @@ expect_equal func($T type, expected, actual T, location SourceLocation) void ! E debug.print("{s}:{d}:{d}: expected and actual slices differ\n", {location.file, location.line, location.column}) return .expectation_failed } - else: if expected != actual { - debug.print("{s}:{d}:{d}: expected {}, found {}\n", {location.file, location.line, location.column, expected, actual}) - return .expectation_failed + else: { + if expected != actual { + debug.print("{s}:{d}:{d}: expected {}, found {}\n", {location.file, location.line, location.column, expected, actual}) + return .expectation_failed + } } } }