feat: execute existing hash maps through Honey

This commit is contained in:
2026-10-30 11:46:49 +01:00
parent 31a1bf1995
commit e90d4f0829
19 changed files with 761 additions and 47 deletions
+519 -1
View File
@@ -11,6 +11,93 @@ HONEY = ROOT / "build/honey"
# Explicit expectations guard against frontend bugs shared by both execution paths.
CASES = {
"unreachable_is_a_demanded_source_trap_without_cleanup": (
"""
fail proc() i16 {
defer unreachable
unreachable
}
main proc() i16 { return fail() }
""", b"", b":3:5: runtime trap: reached unreachable statement",
),
"untaken_and_unused_unreachable_do_not_diagnose": (
"""
unused proc() i16 { unreachable }
choose proc(flag bool) i16 {
if (flag) unreachable
return match flag { true: unreachable, false: 42 }
}
main proc() i16 { return choose(false) }
""", b"42\n", None,
),
"unreachable_traps_only_selected_constant_execution": (
"""
choose proc(flag bool) i16 {
if (flag) unreachable
return 42
}
main proc() i16 { return $(choose(false)) }
""", b"42\n", None,
),
"unreachable_constant_execution_preserves_source": (
"""
fail proc() i16 {
unreachable
}
main proc() i16 { return $(fail()) }
""", b"", b":2:5: runtime trap: reached unreachable statement",
),
"terminal_while_true_preserves_nested_breaks_and_cleanups": (
"""
compute proc(trace @mut i16) i16 {
while true {
defer trace^ += 1
while true { break }
if (trace^ == 2) return 42
continue
}
}
main proc() [2]i16 {
trace i16 := 0
answer :: compute(&trace)
return [answer, trace]
}
""", b"[42, 3]\n", None,
),
"terminal_while_true_return": (
"main proc() i16 { while true { return 42 } }",
b"42\n", None,
),
"terminal_while_true_constant_execution": (
"compute proc() i16 { while true { return 42 } }\nmain proc() i16 { return $(compute()) }",
b"42\n", None,
),
"terminal_while_true_ignores_break_after_return": (
"main proc() i16 { while true { return 42\nbreak } }",
b"42\n", None,
),
"while_true_break_requires_return": (
"main proc() i16 { while true { break } }",
b"", b"nonvoid function can finish without returning a value",
),
"while_true_value_block_break_requires_return": (
"""
main proc() i16 {
while true {
value i16 :: { break }
_ = value
}
}
""", b"", b"nonvoid function can finish without returning a value",
),
"while_runtime_guard_still_requires_return": (
"main proc() i16 { flag := true\nwhile flag { return 42 } }",
b"42\n", b"nonvoid function can finish without returning a value",
),
"unreachable_rejects_trailing_operand": (
"main proc() i16 { unreachable 42 }",
b"", b"unexpected token",
),
"distinct_bitwise_results_preserve_nominal_type": (
"""
Flags :: distinct u16
@@ -7334,6 +7421,126 @@ CASES = {
"invalid_unused_signature": (
"bad proc(a void) void {}\nmain proc() int { return 42 }", b"42\n", b"void cannot be stored",
),
"optional_compile_time_callbacks_preserve_source_identity": (
"""
first proc(value i16) i16 { return value }
second proc(value i16) i16 { return value + 2 }
invoke proc($callback proc(value i16) i16, value i16) i16 { return callback(value) }
choose proc($callback ?proc(value i16) i16, value i16) i16 {
if $(callback == null) return value + 3
else return invoke(callback?, value)
}
main proc() [3]i16 { return [choose(first, 42), choose(second, 40), choose(null, 39)] }
""", b"[42, 42, 42]\n", None,
),
"null_compile_time_callback_unwrap_traps": (
"""
invoke proc($callback proc() i16) i16 { return callback() }
choose proc($callback ?proc() i16) i16 { return invoke(callback?) }
main proc() i16 { return choose(null) }
""", b"", b"null optional",
),
"compile_error_reports_owned_computed_message": (
"""
explain proc() string { return "invalid \\"key\\"" }
fail proc($bad bool) i16 {
if (bad) compile_error!(explain())
return 42
}
main proc() i16 { return fail(true) }
""", b"", b'invalid "key"',
),
"compile_error_skips_unselected_generic_and_type_bodies": (
"""
choose proc($bad bool) i16 {
if (bad) compile_error!("wrong branch")
return 42
}
Storage proc(bad bool) type {
if (bad) compile_error!("wrong type branch")
return i16
}
main proc() Storage(false) { return choose(false) }
""", b"42\n", None,
),
"compile_error_type_execution_preserves_message": (
"""
Storage proc(bad bool) type {
if (bad) compile_error!("invalid generated storage")
return i16
}
main proc() Storage(true) { return 42 }
""", b"", b"invalid generated storage",
),
"compile_error_custom_diagnostics_survive_later_evaluation": (
"""
unused proc() void { compile_error!("retained message") }
compute proc() i16 { return 42 }
main proc() i16 { return $(compute()) }
""", b"42\n", b"error: retained message",
),
"compile_error_rejects_runtime_messages": (
"""
fail proc(message string) void { compile_error!(message) }
main proc() i16 { fail("not constant")\nreturn 42 }
""", b"", b"compile_error! requires one compile-time string argument",
),
"compile_error_rejects_wrong_arity": (
"main proc() i16 { compile_error!()\nreturn 42 }",
b"", b"compile_error! requires one compile-time string argument",
),
"type_execution_unreachable_is_selected_only": (
"""
Storage proc(bad bool) type {
if (bad) unreachable
return i16
}
main proc() Storage(false) { return 42 }
""", b"42\n", None,
),
"type_execution_unreachable_retains_source": (
"""
Storage proc() type {
unreachable
}
main proc() Storage() { return 42 }
""", b"", b":2:5:",
),
"rigid_slice_inference_preserves_borrowing_and_string_context": (
"""
Box proc($T type) type { return struct { value T } }
length proc($T type, box @Box(T), value T) usize { return box.value.len + value.len }
main proc() [3]usize {
box Box([]u8) :: { value = "ab" }
bytes [3]mut u8 := [1, 2, 3]
return [length(&box, bytes[..]), length(&box, "abc"), length(&box, bytes)]
}
""", b"[5, 5, 5]\n", None,
),
"rigid_slice_inference_rejects_readonly_weakening_reversal": (
"""
Box proc($T type) type { return struct { value T } }
write proc($T type, box @Box(T), value T) void { value[0] = 0 }
main proc() i16 {
bytes [1]mut u8 := [42]
box Box([]mut u8) :: { value = bytes[..] }
readonly []u8 :: bytes[..]
write(&box, readonly)
return 42
}
""", b"", b"",
),
"rigid_slice_inference_rejects_incompatible_elements": (
"""
Box proc($T type) type { return struct { value T } }
length proc($T type, box @Box(T), value T) usize { return value.len }
main proc() usize {
box Box([]u8) :: { value = "" }
words [1]i16 :: [42]
return length(&box, words[..])
}
""", b"", b"",
),
}
# Keep malformed schemas grouped: each must fail before generated storage is used.
@@ -9300,6 +9507,315 @@ def check_typeinfo_packages(temporary, executable):
return len(cases) + 4
def check_hashmap_packages(temporary, executable):
project = temporary / "hashmap"
project.mkdir()
(project / "build.hon").write_text("# project root\n")
for package in ("std", "ffi"):
(project / package).symlink_to(ROOT / package, target_is_directory=True)
source = project / "main.hon"
# Each growth allocation has distinct typed storage: clearing new entries
# must not overwrite the old entries before the library rehashes them.
source.write_text(textwrap.dedent("""
import "@std/hashmap"
import "@std/mem"
Value :: struct { number i16, items [2]mut i16 }
Slot :: alias hashmap.Entry(i16, Value)
StringSlot :: alias hashmap.Entry([]u8, Value)
State :: struct {
small *mut Slot
medium *mut Slot
large *mut Slot
strings *mut StringSlot
fail bool = false
allocations usize = 0
releases usize = 0
}
allocate proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
state @mut State :: ptrcast!(context?)
state.allocations += 1
if (state.fail or alignment != alignof!(Slot)) return null
if (size == sizeof!([8]Slot)) return ptrcast!(state.small)
if (size == sizeof!([16]Slot)) return ptrcast!(state.medium)
if (size == sizeof!([32]Slot)) return ptrcast!(state.large)
if (size == sizeof!([8]StringSlot)) return ptrcast!(state.strings)
return null
}
resize proc(
_ ?@mut anyopaque, _ ?*mut u8, _ usize, _ usize, _ usize,
) ?*mut u8 { return null }
release proc(context ?@mut anyopaque, _ ?*mut u8, _ usize, _ usize) void {
state @mut State :: ptrcast!(context?)
state.releases += 1
}
table mem.AllocatorVTable :: { alloc = allocate, realloc = resize, free = release }
collision_hash proc(key i16) usize { return if (key == 0) 0 else 1 }
equivalent proc(left, right i16) bool {
return left - divtrunc!(left, 100) * 100 == right - divtrunc!(right, 100) * 100
}
exercise proc(allocator mem.Allocator, state @mut State) i16 ! (hashmap.PutError | mem.AllocError) {
map := hashmap.init(i16, Value, collision_hash, equivalent, allocator)
defer hashmap.deinit(&map)
if (map.count != 0 or map.entries.len != 0 or state.allocations != 0) return -1
if (hashmap.get(&map, 0) != null) return -2
original Value := { number = 0, items = [0, 1] }
try hashmap.put(&map, 0, original)
original.items[0] = 99
if (hashmap.get(&map, 0)?.items[0] != 0) return -3
copy := hashmap.get(&map, 0)?
copy.items[1] = 99
if (hashmap.get(&map, 100)?.items[1] != 1) return -4
duplicate := false
hashmap.put(&map, 100, Value{ number = 999, items = [9, 9] }) catch |err| {
duplicate = err == .key_exists
}
if (!duplicate or map.count != 1 or hashmap.get(&map, 0)?.number != 0) return -7
for (i16(1)..i16(12)) |key| {
try hashmap.put(&map, key, Value{ number = key * 2, items = [key, key + 1] })
}
if (map.count != 12 or map.entries.len != 16) return -5
if (state.allocations != 2 or state.releases != 1) return -6
state.fail = true
previous :: map.entries.ptr
calls :: state.allocations
released :: state.releases
failed := false
hashmap.put(&map, 12, original) catch |err| { failed = err == .out_of_memory }
if (!failed or map.count != 12 or map.entries.len != 16 or map.entries.ptr != previous) return -8
if (state.allocations != calls + 1 or state.releases != released) return -9
if (hashmap.get(&map, 12) != null or hashmap.get(&map, 111)?.number != 22) return -10
state.fail = false
for (i16(12)..i16(24)) |key| {
try hashmap.put(&map, key, Value{ number = key * 2, items = [key, key + 1] })
}
if (map.count != 24 or map.entries.len != 32) return -11
for (i16(0)..i16(24)) |key| {
value :: hashmap.get(&map, key + 100)?
if (value.number != key * 2 or value.items[0] != key or value.items[1] != key + 1) return -12
}
for map.entries |entry| {
if (entry.hash != 0 and entry.hash != 1) return -13
}
if (hashmap.get(&map, 99) != null) return -14
failed = false
hashmap.put(&map, 24, original) catch |err| { failed = err == .out_of_memory }
if (!failed or map.count != 24 or map.entries.len != 32) return -15
if (hashmap.get(&map, 23)?.number != 46 or hashmap.get(&map, 24) != null) return -16
return 42
}
exercise_automatic_and_string_keys proc(allocator mem.Allocator) i16 ! (hashmap.PutError | mem.AllocError) {
automatic := hashmap.init(i16, Value, null, null, allocator)
defer hashmap.deinit(&automatic)
for (i16(0)..i16(12)) |key| {
try hashmap.put(&automatic, key, Value{ number = key, items = [key, key + 1] })
}
edge Value :: { number = 42, items = [19, 23] }
try hashmap.put(&automatic, minval!(i16), edge)
try hashmap.put(&automatic, maxval!(i16), edge)
for (i16(0)..i16(12)) |key| {
value :: hashmap.get(&automatic, key)?
if (value.number != key or value.items[1] != key + 1) return -20
}
if (hashmap.get(&automatic, minval!(i16))?.number != 42
or hashmap.get(&automatic, maxval!(i16))?.items[0] != 19) return -21
if (hashmap.get(&automatic, -1) != null) return -22
strings hashmap.StringHashMap(Value) := { entries = &[], count = 0, allocator = allocator }
defer hashmap.deinit(&strings)
left [3]mut u8 := [97, 0, 98]
right [3]mut u8 := [97, 0, 98]
try hashmap.put(&strings, left[..], edge)
if (hashmap.get(&strings, right[..])?.items[1] != 23) return -23
duplicate := false
hashmap.put(&strings, right[..], Value{ number = 0, items = [0, 0] }) catch |err| {
duplicate = err == .key_exists
}
if (!duplicate or strings.count != 1 or hashmap.get(&strings, left[..])?.number != 42) return -24
try hashmap.put(&strings, "", edge)
if (hashmap.get(&strings, right[..2]) != null or hashmap.get(&strings, "")?.number != 42) return -25
return 42
}
main proc() i16 ! (hashmap.PutError | mem.AllocError) {
small [8]mut Slot := undefined
medium [16]mut Slot := undefined
large [32]mut Slot := undefined
string_storage [8]mut StringSlot := undefined
state State := {
small = (&small).ptr, medium = (&medium).ptr, large = (&large).ptr,
strings = (&string_storage).ptr,
}
allocator mem.Allocator :: { context = &state, vtable = &table }
result :: try exercise(allocator, &state)
if (result != 42) return result
if (state.allocations != 5 or state.releases != 3) return -17
state.fail = true
empty := hashmap.init(i16, Value, collision_hash, equivalent, allocator)
defer hashmap.deinit(&empty)
failed := false
hashmap.put(&empty, 0, Value{ number = 42, items = [4, 2] }) catch |err| {
failed = err == .out_of_memory
}
if (!failed or empty.count != 0 or empty.entries.len != 0) return -18
if (hashmap.get(&empty, 0) != null or state.allocations != 6 or state.releases != 3) return -19
state.fail = false
automatic_result :: try exercise_automatic_and_string_keys(allocator)
if (automatic_result != 42) return automatic_result
if (state.allocations != 10 or state.releases != 7) return -26
return 42
}
""").strip() + "\n")
check(source, executable, (b"42\n", None))
native_cases = (
"""
import "@std/hashmap"
import "@std/mem"
Sparse :: enum(i16) { negative = -7, first = 3, far = 90 }
SparseAlias :: alias Sparse
Inner :: distinct u64
Id :: distinct Inner
integer_keys proc($K type) bool ! (hashmap.PutError | mem.AllocError) {
map hashmap.AutoHashMap(K, usize) := hashmap.init(K, usize, null, null, mem.c_allocator)
defer hashmap.deinit(&map)
keys [4]K :: [minval!(K), maxval!(K), K(1), K(2)]
for keys |key, index| { try hashmap.put(&map, key, index) }
for (3..40) |number| { try hashmap.put(&map, K(number), usize(number)) }
if (map.count != 41 or map.entries.len != 64) return false
for keys |key, index| { if (hashmap.get(&map, key)? != index) return false }
for (3..40) |number| {
if (hashmap.get(&map, K(number))? != usize(number)) return false
}
if (hashmap.get(&map, K(40)) != null) return false
if $(minval!(K) < 0) {
try hashmap.put(&map, K(-1), 99)
if (hashmap.get(&map, K(-1))? != 99) return false
}
duplicate := false
hashmap.put(&map, maxval!(K), 999) catch |err| { duplicate = err == .key_exists }
return duplicate and hashmap.get(&map, maxval!(K))? == 1
}
main proc() i16 ! (hashmap.PutError | mem.AllocError) {
if (!try integer_keys(i8) or !try integer_keys(u8)) return -1
if (!try integer_keys(i16) or !try integer_keys(u16)) return -2
if (!try integer_keys(i32) or !try integer_keys(u32)) return -3
if (!try integer_keys(i64) or !try integer_keys(u64)) return -4
if (!try integer_keys(isize) or !try integer_keys(usize)) return -5
flags hashmap.AutoHashMap(bool, i16) := hashmap.init(bool, i16, null, null, mem.c_allocator)
defer hashmap.deinit(&flags)
try hashmap.put(&flags, false, 19)
try hashmap.put(&flags, true, 23)
if (flags.count != 2 or hashmap.get(&flags, false)? != 19 or hashmap.get(&flags, true)? != 23) return -6
duplicate := false
hashmap.put(&flags, false, 99) catch |err| { duplicate = err == .key_exists }
if (!duplicate or flags.count != 2 or hashmap.get(&flags, false)? != 19) return -7
sparse hashmap.AutoHashMap(SparseAlias, i16) := hashmap.init(SparseAlias, i16, null, null, mem.c_allocator)
defer hashmap.deinit(&sparse)
try hashmap.put(&sparse, .negative, 7)
try hashmap.put(&sparse, .first, 3)
try hashmap.put(&sparse, .far, 90)
if (sparse.count != 3 or hashmap.get(&sparse, Sparse.negative)? != 7) return -8
if (hashmap.get(&sparse, .first)? != 3 or hashmap.get(&sparse, .far)? != 90) return -9
ids hashmap.AutoHashMap(Id, i16) := hashmap.init(Id, i16, null, null, mem.c_allocator)
defer hashmap.deinit(&ids)
try hashmap.put(&ids, Id(Inner(0)), 19)
try hashmap.put(&ids, Id(Inner(maxval!(u64))), 23)
if (hashmap.get(&ids, Id(Inner(0)))? != 19) return -10
if (hashmap.get(&ids, Id(Inner(maxval!(u64))))? != 23) return -11
if (hashmap.get(&ids, Id(Inner(1))) != null) return -12
return 42
}
""",
"""
import "@std/hashmap"
import "@std/mem"
Padding :: struct_type!(.c, ["tag", "number"], [u8, u64], [null, null])
Row :: struct { values [2]u16 }
Key :: struct { padded Padding, rows [2]Row }
custom_hash proc(_ u16) usize { return 0 }
custom_eql proc(left, right u16) bool {
return left - divtrunc!(left, 100) * 100 == right - divtrunc!(right, 100) * 100
}
main proc() i16 ! (hashmap.PutError | mem.AllocError) {
# The alias supplies the library's private string callbacks.
strings hashmap.StringHashMap(i16) := { entries = &[], count = 0, allocator = mem.c_allocator }
defer hashmap.deinit(&strings)
left [3]mut u8 := [97, 0, 98]
right [3]mut u8 := [97, 0, 98]
other [3]mut u8 := [97, 0, 99]
if ((&left).ptr == (&right).ptr) return -7
try hashmap.put(&strings, left[..], 42)
records hashmap.AutoHashMap(Key, i16) := hashmap.init(Key, i16, null, null, mem.c_allocator)
defer hashmap.deinit(&records)
first Key := { padded = { tag = 7, number = maxval!(u64) }, rows = [{ values = [2, 9] }, { values = [4, 8] }] }
same Key := { padded = { tag = 7, number = maxval!(u64) }, rows = [{ values = [2, 9] }, { values = [4, 8] }] }
first_bytes *mut u8 :: ptrcast!(&first.padded)
same_bytes *mut u8 :: ptrcast!(&same.padded)
if (sizeof!(Padding) != 16 or alignof!(Padding) != 8) return -1
for (usize(1)..usize(8)) |index| {
first_bytes[index] = 17
same_bytes[index] = 238
}
if (first_bytes[1] == same_bytes[1]) return -2
try hashmap.put(&records, first, 19)
if (hashmap.get(&records, same)? != 19) return -3
duplicate := false
hashmap.put(&records, same, 99) catch |err| { duplicate = err == .key_exists }
if (!duplicate or records.count != 1 or hashmap.get(&records, first)? != 19) return -4
reversed :: Key{ padded = { tag = 7, number = maxval!(u64) }, rows = [{ values = [9, 2] }, { values = [4, 8] }] }
swapped :: Key{ padded = { tag = 7, number = maxval!(u64) }, rows = [{ values = [4, 8] }, { values = [2, 9] }] }
try hashmap.put(&records, reversed, 23)
try hashmap.put(&records, swapped, 31)
if (hashmap.get(&records, reversed)? != 23 or hashmap.get(&records, swapped)? != 31) return -5
same.padded.number = 0
if (hashmap.get(&records, same) != null or records.count != 3) return -6
if (hashmap.get(&strings, right[..])? != 42) return -8
if (hashmap.get(&strings, right[..2]) != null or hashmap.get(&strings, other[..]) != null) return -9
duplicate = false
hashmap.put(&strings, right[..], 99) catch |err| { duplicate = err == .key_exists }
if (!duplicate or strings.count != 1 or hashmap.get(&strings, left[..])? != 42) return -10
try hashmap.put(&strings, "", 7)
try hashmap.put(&strings, other[..], 23)
if (strings.count != 3 or hashmap.get(&strings, "")? != 7 or hashmap.get(&strings, other[..])? != 23) return -11
custom := hashmap.init(u16, i16, custom_hash, custom_eql, mem.c_allocator)
defer hashmap.deinit(&custom)
for (u16(1)..u16(20)) |key| { try hashmap.put(&custom, key, i16(key * 2)) }
for (u16(1)..u16(20)) |key| {
if (hashmap.get(&custom, key + 100)? != i16(key * 2)) return -12
}
duplicate = false
hashmap.put(&custom, 101, 999) catch |err| { duplicate = err == .key_exists }
if (!duplicate or custom.count != 19 or hashmap.get(&custom, 1)? != 2) return -13
if (hashmap.get(&custom, 99) != null) return -14
return 42
}
""",
)
for text in native_cases:
source.write_text(textwrap.dedent(text).strip() + "\n")
interpreted = run(str(HONEY), "run", str(source))
assert interpreted.returncode == 1 and interpreted.stdout == b"", outcome(interpreted)
assert b"foreign function calls are unsupported by the interpreter" in interpreted.stderr, outcome(interpreted)
assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted)
for optimization in ("-O0", "-O2"):
built = run(str(HONEY), "build", str(source), "-o", str(executable), optimization)
assert outcome(built) == (0, b"", b""), outcome(built)
assert outcome(run(str(executable))) == (0, b"42\n", b""), optimization
executable.unlink()
invalid_keys = (("f64", "f64(1)"), ("[]u8", '"abc"'), ("?i16", "some!(i16(1))"))
for key_type, key in invalid_keys:
source.write_text(textwrap.dedent(f"""
import "@std/hashmap"
import "@std/mem"
main proc() bool {{
map := hashmap.init({key_type}, i16, null, null, mem.c_allocator)
return hashmap.get(&map, {key}) == null
}}
""").strip() + "\n")
check(source, executable, (
b"true\n",
b"AutoHashMap key requires integers, booleans, enums, distinct types, arrays or records",
))
return 1 + len(invalid_keys), len(native_cases)
def main():
with tempfile.TemporaryDirectory(prefix="honey native ' ; $") as directory:
temporary = Path(directory)
@@ -9316,6 +9832,8 @@ def main():
allocator_count = check_allocator_libc(temporary, executable)
arraylist_count, arraylist_native_count = check_arraylist_packages(temporary, executable)
count += arraylist_count
hashmap_count, hashmap_native_count = check_hashmap_packages(temporary, executable)
count += hashmap_count
count += check_packages(temporary, executable)
count += check_namespace_packages(temporary, executable)
count += check_record_packages(temporary, executable)
@@ -9342,7 +9860,7 @@ def main():
assert not executable.exists()
invalid = run(str(HONEY), "build", str(source), "-o", str(executable), "-O3")
assert invalid.returncode == 2 and b"usage:" in invalid.stderr, outcome(invalid)
print(f"{count} programs matched interpreter at -O0 and -O2; {allocator_count + arraylist_native_count} native allocator programs, LLVM output and CLI errors passed")
print(f"{count} programs matched interpreter at -O0 and -O2; {allocator_count + arraylist_native_count + hashmap_native_count} native allocator programs, LLVM output and CLI errors passed")
if __name__ == "__main__":