From e90d4f08295b29c744ee35722dfae5d1e983106c Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 30 Oct 2026 11:46:49 +0100 Subject: [PATCH] feat: execute existing hash maps through Honey --- README.md | 10 +- TODO.md | 9 +- source/ast/ast.hon | 11 +- source/ast/renderer/renderer.hon | 1 + source/diagnostics/diagnostics.hon | 26 ++ source/interpreter/interpreter.hon | 2 +- source/lexer/scan.hon | 1 + source/lexer/token.hon | 1 + source/llvm/llvm.hon | 8 +- source/lower/lower.hon | 21 +- source/lower/lower.test.hon | 4 +- source/main.hon | 2 +- source/parser/parse.hon | 17 +- source/sema/check/check.hon | 56 ++- source/sema/check/check.test.hon | 21 +- source/sema/resolve.hon | 2 +- source/sema/specialize/specialize.hon | 88 ++++- std/hashmap/hashmap.hon | 8 +- tests/native.py | 520 +++++++++++++++++++++++++- 19 files changed, 761 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 8cc04cb..1183d0c 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ ordinary procedures may compute integer, boolean, float and string compile-time `if $(condition) { ... } else { ... }` evaluates a boolean guard before expanding or checking either body, including calls to ordinary constant-input procedures. concrete generic specializations also select branches whose guards use constants, immutable constant bindings, type comparisons or substituted compile-time parameters. only the selected body is specialized and checked; both bodies are still parsed and name-resolved. selected bodies retain their scope, cleanup and return behavior, and erased guards retain ordinary type checking and source diagnostics. ordinary calls in an implicit guard stay runtime; use `$()` to request their evaluation. optional captures remain runtime, and ordinary nongeneric `if (true)` still checks both bodies. +`compile_error!(message)` requires a compile-time string and reports its owned text at the call site when that body is elaborated. unselected compile-time branches do not report it; ordinary runtime branches remain checked. messages from constant-input procedures retain their bytes through later evaluation, interpreter diagnostics and native executable emission. + `match $(subject) { ... }` selects an expression or statement arm before specializing or checking its body, including ordinary constant-input calls. concrete generic procedures also select matches on side-effect-free constant subjects, including substituted scalar/enum/tagged-union arguments and pure field/index projections. ordinary checked IR validates every pattern and capture declaration before selection; only the selected body is elaborated, while all bodies remain parsed and name-resolved. by-value payload captures retain exact types and owned strings, and selected bodies may use runtime parameters, nested matches, cleanup, returns and enclosing-loop transfers. ordinary call subjects, runtime parameters and mutable locals stay runtime, as do contextual aggregate bindings that might omit runtime defaults; ordinary nongeneric literal matches still check all arms. use `$()` to request explicit evaluation. pointer/slice/callback-bearing payloads retain the escape boundary above. type `==` and `!=` comparisons in compile-time expressions and specializations preserve aliases, nominal declaration identity and structural pointer/array/callback signatures. composed error-set identity and specialization caching ignore member ordering, grouping and repetition. for example, `read proc($T type, value T) i16 { if (T == i16) return value else return value.count }` checks the scalar or record path for each concrete call without requiring both operations on every `T`. @@ -192,7 +194,9 @@ fallible values use explicit `T ! E` channels, where `E` is an enum, record or t native external declarations use `malloc c_func(size c_ulong) ?*mut anyopaque`, without a body. supported signatures contain concrete integer/float/boolean scalars, data/function pointers, nullable pointers and `void` results. generated bindings may declare `c_struct` records, nominal pointer-only `opaque` types and `*c_func(...) Result` callback signatures. unused foreign declarations do not force ABI validation or native symbol emission; demanding a variadic or aggregate-by-value signature diagnoses unsupported operands. Darwin `c_char`/`c_uchar`, `c_short`/`c_ushort`, `c_int`/`c_uint`, `c_long`/`c_ulong` and `c_longlong`/`c_ulonglong` map to the corresponding 8/16/32/64-bit integer representations; `c_double` maps to `f64`, while demanded `c_float` and `c_longdouble` types remain unsupported. foreign calls trap only when reached in the interpreter; arbitrary interpreter FFI is not supported. -the existing `std/arraylist` and `std/mem` work without replacement bindings or library code. interpreter/native checks cover initialization, reserve/growth, append/pop/clear, allocation failure without losing the original list, zero-sized elements and deferred cleanup on error. native checks use the real `mem.c_allocator` and imported `ffi/c` declarations for aggregate element storage and over-aligned allocation/reallocation; interpreter allocation checks use a fixed-capacity test allocator, not foreign calls. hash maps, broader metadata operations and formatted IO remain later acceptance gates. +the existing `std/arraylist` and `std/mem` work without replacement bindings or library code. interpreter/native checks cover initialization, reserve/growth, append/pop/clear, allocation failure without losing the original list, zero-sized elements and deferred cleanup on error. native checks use the real `mem.c_allocator` and imported `ffi/c` declarations for aggregate element storage and over-aligned allocation/reallocation; interpreter allocation checks use a fixed-capacity test allocator, not foreign calls. broader metadata operations and formatted IO remain later acceptance gates. + +existing `std/hashmap.HashMap`, `AutoHashMap` and `StringHashMap` execute without replacement library code. checks cover collision chains, zero-hash normalization, growth/rehashing, duplicate-key errors, absent lookups, aggregate copy isolation, allocation failure preserving the map and deferred cleanup. native checks use the real C allocator for integer boundaries, booleans, sparse/aliased enums, nested distinct/array/record keys (ignoring record padding), content-based strings and custom callbacks; interpreter checks use separate fixed-capacity typed buffers. pass initializer parameters explicitly, such as `hashmap.init(i16, i16, null, null, allocator)`; initializer inference from an expected result type remains open. a typed `StringHashMap` literal retains its private library callbacks. records use `Point :: struct { x i16, y i16 = 2 }`, with comma- or newline-separated fields. named record declarations have nominal identity; two separately declared records are not interchangeable. construct them with `Point { x = 40 }` or a contextual keyed literal, such as `point Point := { x = 40 }`. omitted fields use their declared defaults; missing required fields, duplicate/unknown fields, incompatible values and recursive by-value layouts are diagnosed. explicit initializers run in source order, followed by omitted defaults in declaration order. an explicit value overrides its field's default without evaluating that default. @@ -225,6 +229,10 @@ composed error sets use the same inline arms, including through `catch |err| mat `while` repeats a block or single statement while its `bool` condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as `if`. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry. +a literal `while true` without a reachable break is terminal, including when only nested loops break. nonvoid functions need no fallthrough return after it; runtime guards and loops with reachable breaks still require a return. + +`unreachable` is an operand-free statement that traps only when reached, preserving its source location in interpreted, native and compile-time execution. untaken and unused occurrences do not diagnose. it is a fatal trap, not a source error value or a cleanup transfer; it does not execute deferred cleanup. + `for 0..10 |i| { ... }` iterates an ascending, upper-exclusive integer range; `..=` includes the upper bound. a capture is required: `|i|` binds an immutable value with the bounds' exact integer type in the body scope, while `|_|` discards it. bounds resolve in the enclosing scope and are evaluated left-to-right once, so changing a bound variable in the body does not change the range. reversed ranges are empty; equal bounds produce zero iterations with `..` and one with `..=`. inclusive ranges terminate safely at the maximum of each integer type. range bounds that are compound or unary expressions require their own parentheses, such as `(-2)..(limit + 1)`. a braceless body also requires parentheses around the entire iterable: `for (0..10) |i| total = total + i`. first-class range values and range pointer/index captures remain unimplemented. [programs/range_loop.hon](programs/range_loop.hon) skips `5` while summing `0..10`; `./build/honey run programs/range_loop.hon` prints `40`. diff --git a/TODO.md b/TODO.md index 38158b5..810d8b2 100644 --- a/TODO.md +++ b/TODO.md @@ -56,6 +56,12 @@ runtime `undefined` locals and compute fully initialized materializable constant the existing `ArrayList` uses nested generic inference, file-local helpers and the real imported C allocator through native execution; interpreter checks use non-FFI allocators and cover zero-sized storage, growth, failure and cleanup. +existing hash maps execute collision chains, rehashing, duplicate/absent lookups, +aggregate copies and allocator failure/cleanup through both paths; native checks +cover automatic integer/bool/enum/distinct/array/record keys, content-based strings +and custom callbacks with the real C allocator. initializer parameters remain explicit. +`unreachable` is a demanded source trap; terminal literal-true loops need no return. +`compile_error!` retains owned user messages and skips unselected compile-time bodies. boolean and optional-capture `if ... else` expressions reuse checked matches for runtime/constant values, contextual optional/fallible results and generated types. yielding value blocks, match arms and catch handlers copy results before scoped @@ -133,7 +139,8 @@ test discovery and FFI remain outside these completed slices. - [x] optional conditional-expression checkpoint: `if value |payload| then_value else else_value` reuses canonical presence matches through interpreter/native execution. single-evaluation guards and lazy branches preserve exact immutable by-value captures, nested optional presence, aggregate isolation and runtime pointer/callback/slice/zero-sized payloads. contextual optional/fallible results, constant-input procedures, generated types/defaults and explicit compile-time subject selection retain checked execution and owned payload snapshots. nonoptional guards, capture scope/shadowing, malformed captures, missing `else`, incompatible branches and compile-time overflow retain diagnostics. yielding value blocks are covered below; broader constant materialization remains open. - [x] yielding-block checkpoint: value blocks and `yield expression` produce exact/contextual results through interpreter/native execution, including conditional branches, match arms and catch handlers. nested value sources select the nearest target; ordinary blocks/loops preserve it. yielded aggregates snapshot before LIFO cleanup, successful yields skip `errdefer`, and return/error/break/continue retain enclosing targets. ordinary constants, generic branch specialization and type-producing blocks generate owned values, types, storage and defaults. missing operands/results, incompatible yields, capture mutation and outward cleanup transfers retain diagnostics and demand-driven traps; imported provenance and repeatable compilation are verified. labeled yields and value-producing loops remain outside this checkpoint. - [x] compound/bitwise checkpoint: arithmetic and bitwise compound updates evaluate mutable local/field/index/pointer places and their old values once before the RHS through interpreter/native execution. integer complement, and/or/xor, truncating left shifts, signed/unsigned right shifts and saturating left shifts preserve widths and distinct identity; unsigned counts retain independent types. known invalid counts diagnose even in untaken runtime branches; dynamic counts trap without native LLVM poison. checked arithmetic, float-only division, readonly diagnostics, scoped cleanup, ordinary constants, generated storage, generic branch selection and repeatable compilation retain existing semantics. type-valued `|` remains composition while runtime `|` and capture delimiters retain their grammar. full hash-map acceptance remains open. - - [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including untagged unions and storage-bearing values), plus full existing-library acceptance for hash maps, broader `meta` operations and `io.print`. formatting parsing and broader imported C-binding support may interleave with later gates. + - [x] hash-map execution checkpoint: existing `std/hashmap.HashMap`, `AutoHashMap` and `StringHashMap` run collision chains, zero-hash normalization, growth/rehashing, duplicate-key errors, absent lookups and owned aggregate values without replacement library code. interpreter checks use separate typed allocation buffers and preserve the original map on allocation failure, including cleanup; native `mem.c_allocator` checks cover all integer widths/boundaries, booleans, sparse/aliased enums, nested distinct/array/record keys with padding ignored, content-based byte strings and custom callbacks at `-O0`/`-O2`. specialization retains `usize` collection indices, nominal/explicit pointer-cast result types, rigid slice borrowing/string context and source-known optional callbacks; ordinary checking rejects incompatible elements and readonly reversal. `unreachable` demand-traps with provenance through both paths and compile-time execution; literal-true loops distinguish escaping and nested breaks. `compile_error!` owns computed string diagnostics and skips unselected compile-time bodies. initializer inference from expected result types remains open. + - [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including untagged unions and storage-bearing values), expected-result generic inference (including hash-map initializers), plus remaining existing-library acceptance for broader `meta` operations and `io.print`. formatting parsing and broader imported C-binding support may interleave with later gates. - [ ] remaining source constructs: multiline strings. boolean and optional-capture conditional expressions, yielding value blocks, `yield`, compound updates and bitwise operators are covered above. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. - [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library. - [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index 2c9d6bb..741c36b 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -14,7 +14,7 @@ #! * arm ...... -> block | stmt (single statements become canonical blocks) #! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated. #! enum members and tagged-union variants accept keyword names; member names do not enter lexical scopes. -#! * stmt ..... -> decl | expr | block | ('defer' | 'errdefer' ['|' identifier '|']) arm | expr assignment_op expr | 'return' [expr] | 'yield' expr | 'break' | 'continue' | 'if' expr ['|' identifier '|'] arm ['else' arm] | 'while' expr ['|' identifier '|'] arm | ['inline'] 'for' expr '|' ['@'] identifier [',' identifier] '|' arm +#! * stmt ..... -> decl | expr | block | ('defer' | 'errdefer' ['|' identifier '|']) arm | expr assignment_op expr | 'return' [expr] | 'yield' expr | 'break' | 'continue' | 'unreachable' | 'if' expr ['|' identifier '|'] arm ['else' arm] | 'while' expr ['|' identifier '|'] arm | ['inline'] 'for' expr '|' ['@'] identifier [',' identifier] '|' arm #! assignment_op is '=' or a contiguous compound: '+=', '-=', '*=', '/=', '&=', '|=', 'xor=', '<<=', '>>=', '<<|='. #! braceless guards/iterables require parentheses unless they are calls, explicit comptime expressions or optional captures; else binds to the nearest if. bitwise-OR headers require parentheses before captures. #! precedence (tightest first): calls/indices/selections/dereference/unwrap, unary -, !, ~, &, try higher than multiplication/division, addition/subtraction, shifts << / >> / <<|, bitwise &/xor/|, relational comparisons, equality, and, or, ranges/catch. type composition | binds tighter than fallible !; pointer/optional/array prefixes bind to type atoms. compound range endpoints require their own parentheses. @@ -104,6 +104,7 @@ StmtKind :: enum { yield_stmt break_stmt continue_stmt + unreachable_stmt if_stmt while_stmt for_stmt @@ -201,6 +202,7 @@ NodeKind :: enum { literal_undefined expr_block stmt_yield + stmt_unreachable } @hide:file @@ -999,6 +1001,10 @@ add_continue_stmt proc(builder @mut Builder, span Span) NodeId ! mem.AllocError return try add_node(builder, Node{ kind = .stmt_continue }, span) } +add_unreachable_stmt proc(builder @mut Builder, span Span) NodeId ! mem.AllocError { + return try add_node(builder, Node{ kind = .stmt_unreachable }, span) +} + add_if_stmt proc( builder @mut Builder, span Span, @@ -1352,6 +1358,7 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind { .stmt_yield: .yield_stmt .stmt_break: .break_stmt .stmt_continue: .continue_stmt + .stmt_unreachable: .unreachable_stmt .stmt_if: .if_stmt .stmt_assignment: .assignment_stmt .stmt_while: .while_stmt @@ -2060,7 +2067,7 @@ node_layout proc(node Node) NodeLayout { if (node.kind == .stmt_defer or node.kind == .stmt_errdefer) layout.direct_children = 3 } - .stmt_break, .stmt_continue, .invalid_stmt: layout.class = .statement + .stmt_break, .stmt_continue, .stmt_unreachable, .invalid_stmt: layout.class = .statement .stmt_assignment: { layout.class = .statement layout.direct_children = 3 diff --git a/source/ast/renderer/renderer.hon b/source/ast/renderer/renderer.hon index 930289b..24d9197 100644 --- a/source/ast/renderer/renderer.hon +++ b/source/ast/renderer/renderer.hon @@ -65,6 +65,7 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept } .break_stmt: try append_text(renderer, "break\n") .continue_stmt: try append_text(renderer, "continue\n") + .unreachable_stmt: try append_text(renderer, "unreachable\n") .block_stmt: { try append_text(renderer, "block\n") try render_block(renderer, tree, ast.statement_block(tree, statement), depth + 1) diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 84cf2e2..5b8f77d 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -127,11 +127,15 @@ Code :: enum { invalid_metadata_abi invalid_undefined invalid_shift_count + unreachable_executed + compile_error + invalid_compile_error } Diagnostic :: struct { code Code span Span + custom_message ?[]u8 = null } #! owns diagnostic storage and retains its allocator; release with deinit. @@ -146,6 +150,9 @@ init proc(allocator mem.Allocator) Store { #! releases diagnostic storage with the retained allocator. deinit proc(store @mut Store) void { + for (store.list.items) |diagnostic| { + if (diagnostic.custom_message) |text| mem.free(store.list.allocator, text) + } arraylist.deinit(&store.list) } @@ -157,6 +164,22 @@ add proc(store @mut Store, code Code, span Span) DiagnosticId ! (mem.AllocError return id } +#! copies `text`; the store owns the message until deinit. +add_message proc(store @mut Store, code Code, span Span, text []u8) DiagnosticId ! (mem.AllocError | CapacityError) { + owned :: try mem.alloc(u8, store.list.allocator, text.len) + errdefer mem.free(store.list.allocator, owned) + memcopy!(owned, text) + id :: try add(store, code, span) + store.list.items[usize(id)].custom_message = owned + return id +} + +#! borrows either the store-owned custom message or process-lifetime code text. +message proc(diagnostic Diagnostic) []u8 { + if (diagnostic.custom_message) |text| return text + return enummap.get(Code, []u8, &messages, diagnostic.code)? +} + get proc(store @Store, id DiagnosticId) ?Diagnostic { if (usize(id) >= store.list.items.len) return null return store.list.items[usize(id)] @@ -185,6 +208,7 @@ messages :: $enummap.init(Code, []u8, { argument_count_mismatch = "argument count does not match the function signature", cannot_infer_type = "cannot infer a type without a type anchor", missing_return = "nonvoid function can finish without returning a value", + unreachable_executed = "reached unreachable statement", return_type_mismatch = "return does not match the function result type", yield_outside_block = "yield requires an enclosing value block", missing_yield = "nonvoid value block can finish without yielding a value", @@ -275,5 +299,7 @@ messages :: $enummap.init(Code, []u8, { missing_metadata_import = "typeinfo! requires importing @std/meta", invalid_metadata_abi = "@std/meta declarations do not match the compiler reflection ABI", invalid_undefined = "undefined requires a concrete mutable local declaration", + compile_error = "explicit compile-time error", + invalid_compile_error = "compile_error! requires one compile-time string argument", }) diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 65395b8..65aabb2 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -191,7 +191,7 @@ execute proc( if instruction_index == block.instructions.items.len { terminator :: block.terminator if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic } - if (terminator.kind == .unreachable) unreachable + if (terminator.kind == .unreachable) return try runtime_trap(diagnostic_store, .unreachable_executed, terminator.span) if (terminator.kind == .jump or terminator.kind == .branch) { target := terminator.target if (terminator.kind == .branch) { diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index a51c4c8..bb213dd 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -29,6 +29,7 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([ { "yield", .yield }, { "break", .break }, { "continue", .continue }, + { "unreachable", .unreachable }, { "try", .try }, { "catch", .catch }, { "defer", .defer }, diff --git a/source/lexer/token.hon b/source/lexer/token.hon index 9483b35..54a40a9 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -25,6 +25,7 @@ TokenKind :: enum { proc, return yield break, continue + unreachable try, catch defer, errdefer true, false diff --git a/source/llvm/llvm.hon b/source/llvm/llvm.hon index 584fa14..3d4fa08 100644 --- a/source/llvm/llvm.hon +++ b/source/llvm/llvm.hon @@ -61,7 +61,7 @@ emit proc( } for (diagnostic_store.list.items) |diagnostic, index| { try io.print("@d{} = private constant ", output, {index}) - try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?, false) + try bytes_constant(output, diagnostics.message(diagnostic), false) try io.write_all(output, "\n") } memory_codes [_]diagnostics.Code :: [ @@ -69,6 +69,7 @@ emit proc( .overlapping_memory_copy, .unsupported_foreign_call, .invalid_pointer_access, .division_by_zero, .invalid_shift_count, + .unreachable_executed, ] for (memory_codes) |code| { try io.print("@memory_{s} = private constant ", output, {enummap.get(&memory_symbols, code)?}) @@ -321,7 +322,7 @@ emit_reporting proc( try io.write_all(output, "define internal void @report(i64 %trap) {\nentry:\n") for (diagnostic_store.list.items) |diagnostic, index| { position :: location(line_starts, diagnostic.span) - message :: enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)? + message :: diagnostics.message(diagnostic) try io.print(" %t{} = icmp eq i64 %trap, {}\n", output, {index, index}) try io.print(" call void @diagnose(i64 {}, i64 {}, i64 {}, ptr @d{}, i64 {}, i1 %t{})\n", output, { usize(diagnostic.span.source), position.line, position.column, index, message.len, index, @@ -511,6 +512,7 @@ memory_symbols :: $enummap.init_default(diagnostics.Code, []u8, "", { invalid_pointer_access = "invalid_pointer_access", division_by_zero = "division_by_zero", invalid_shift_count = "invalid_shift_count", + unreachable_executed = "unreachable_executed", }) @hide:file @@ -650,7 +652,7 @@ emit_function proc(state @mut State, function @ir.Function, id usize) void ! io. } } .trap: try io.print(" call void @trap(i64 {}, i64 0, i64 0, i64 0, ptr null, i64 0)\n unreachable\n", output, {u32(terminator.diagnostic)}) - .unreachable: try io.write_all(output, " unreachable\n") + .unreachable: try trap(state, .unreachable_executed, terminator.span) .jump: try io.print(" br label %b{}\n", output, {u32(terminator.target)}) .branch: { condition :: try load(state, terminator.value) diff --git a/source/lower/lower.hon b/source/lower/lower.hon index af02f9e..9f954d4 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -23,6 +23,7 @@ LoopTargets :: struct { continue_target ir.BlockId exit ir.BlockId cleanup_depth usize + break_reached bool = false } @hide:file @@ -770,7 +771,14 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn } } state.block = exit - state.terminated = false + state.terminated = ( + loop.capture == null + and ast.expr_kind(state.tree, loop.condition) == .literal_bool + and mem.eql(ast.literal_text(state.tree, loop.condition), "true") + and !state.loop?.break_reached + ) + if (state.terminated) + state.module.functions.items[state.function].blocks.items[usize(exit)].terminator = ir.Terminator{ kind = .unreachable, span = loop.span } } @hide:file @@ -920,6 +928,12 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d .if_stmt: try lower_if(state, id) .while_stmt: try lower_while(state, id) .for_stmt: try lower_for(state, id) + .unreachable_stmt: { + state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ + kind = .unreachable, span = ast.node_span(tree, id), + } + state.terminated = true + } .break_stmt, .continue_stmt: { if (trap_type(state, id)) return if (state.loop) |targets| { @@ -928,6 +942,11 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d if (ast.stmt_kind(tree, id) == .continue_stmt) target = targets.continue_target try lower_cleanups(state, targets.cleanup_depth, ir.no_value) if (state.terminated) return + if (ast.stmt_kind(tree, id) == .break_stmt) { + reached := targets + reached.break_reached = true + state.loop = reached + } state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ kind = .jump, target = target, span = ast.node_span(tree, id), } diff --git a/source/lower/lower.test.hon b/source/lower/lower.test.hon index ccaf1dc..9e2a8f1 100644 --- a/source/lower/lower.test.hon +++ b/source/lower/lower.test.hon @@ -992,7 +992,7 @@ zero_iteration_body_errors_are_checked_but_not_executed test { } } -loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_overflow test { +terminal_true_loops_and_assignment_arithmetic_preserve_runtime_behavior test { sources [5][]u8 :: [ "main proc() int {\n while true { return 42 }\n}\n", "main proc() int {\n while false { return 42 }\n}\n", @@ -1005,7 +1005,7 @@ loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_over defer diagnostics.deinit(&diagnostic_store) module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) - if (index != 2) { + if (index != 0 and index != 2) { try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? try testing.expect_equal(diagnostics.Code.missing_return, diagnostic.code) diff --git a/source/main.hon b/source/main.hon index 026f6b8..fd64c4a 100644 --- a/source/main.hon +++ b/source/main.hon @@ -288,7 +288,7 @@ report_diagnostics proc( line + 1, offset - starts[line] + 1, label, - enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?, + diagnostics.message(diagnostic), }) } } diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 10e0502..8a33746 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -323,7 +323,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError .for, .inline: return try parse_for(state, allow_else) .return: return try parse_return(state, allow_else) .yield: return try parse_yield(state, allow_else) - .break, .continue: return try parse_loop_control(state, allow_else) + .break, .continue, .unreachable: return try parse_loop_control(state, allow_else) .defer, .errdefer: return try parse_cleanup(state, allow_else) .open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state)) .ident, .int, .float, .string, .true, .false, .minus, .bang, .tilde, .open_paren, .open_bracket, .struct, .c_struct, .opaque, .c_func, @@ -426,6 +426,8 @@ parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseErr if (keyword.kind == .break) return try ast.add_break_stmt(state.builder, keyword.span) + if (keyword.kind == .unreachable) + return try ast.add_unreachable_stmt(state.builder, keyword.span) return try ast.add_continue_stmt(state.builder, keyword.span) } @@ -688,6 +690,7 @@ parse_guard proc(state @mut State) ast.NodeId ! ParseError { and !next_is(state, .if) and !next_is(state, .while) and !next_is(state, .for) and !next_is(state, .inline) and !next_is(state, .break) and !next_is(state, .continue) + and !next_is(state, .unreachable) ) { if (next_is(state, .ident) and is_assignment_operator(state.tokens[state.next_token + 1].kind)) break state.next_token += 1 @@ -958,7 +961,7 @@ member_names :: $enummap.init_default(lexer.TokenKind, bool, false, { struct = true, c_struct = true, opaque = true, enum = true, union = true, distinct = true, alias = true, match = true, import = true, test = true, c_func = true, null = true, undefined = true, try = true, catch = true, - defer = true, errdefer = true, + defer = true, errdefer = true, unreachable = true, }) @hide:file @@ -1192,7 +1195,7 @@ parse_match_arm proc(state @mut State) ast.NodeId ! ParseError { state.match_arm_body = true defer state.match_arm_body = previous statement_body :: match current(state).kind { - .open_curly, .return, .yield, .break, .continue, .if, .while, .for, .inline, .defer, .errdefer: true + .open_curly, .return, .yield, .break, .continue, .unreachable, .if, .while, .for, .inline, .defer, .errdefer: true else: false } body ast.NodeId := undefined @@ -1773,7 +1776,7 @@ brace_starts_block proc(state @State) bool { if (kind == .eof) return false if (depth > 0 and (state.tokens[first].kind == .if or state.tokens[first].kind == .open_curly) - and (kind == .yield or kind == .return or kind == .defer or kind == .errdefer) + and (kind == .yield or kind == .return or kind == .unreachable or kind == .defer or kind == .errdefer) and state.tokens[index - 1].kind != .dot and !(index + 1 < state.tokens.len and state.tokens[index + 1].kind == .equal)) return true if (depth == 1) { @@ -1782,7 +1785,7 @@ brace_starts_block proc(state @State) bool { previous :: if (index > first) state.tokens[index - 1].kind else lexer.TokenKind.open_curly if (previous != .dot and !(index + 1 < state.tokens.len and state.tokens[index + 1].kind == .equal)) { match kind { - .yield, .return, .break, .continue, .while, .for, .inline, .defer, .errdefer: return true + .yield, .return, .break, .continue, .unreachable, .while, .for, .inline, .defer, .errdefer: return true else: {} } } @@ -1914,7 +1917,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError { @hide:file is_expression_boundary proc(kind lexer.TokenKind) bool { return match kind { - .close_paren, .newline, .close_curly, .open_curly, .else, .return, .yield, .if, .while, .for, .inline, .defer, .errdefer, .eof: true + .close_paren, .newline, .close_curly, .open_curly, .else, .return, .yield, .unreachable, .if, .while, .for, .inline, .defer, .errdefer, .eof: true else: false } } @@ -2054,7 +2057,7 @@ recovery_closes_delimiters proc(state @State, paren_depth, square_depth usize) b kind :: state.tokens[i].kind match kind { .open_curly, .close_curly, .eof, .if, .else, .while, .for, .inline, - .return, .break, .continue, .defer, .errdefer: return false + .return, .break, .continue, .unreachable, .defer, .errdefer: return false .ident: { if (i + 1 < state.tokens.len) { next :: state.tokens[i + 1].kind diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 6b4c587..9e70746 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -1101,6 +1101,7 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b try collect_declaration(state, statement) } .expression: try collect_expression(state, ast.statement_expr(tree, statement)) + .unreachable_stmt: state.cells[usize(statement)].type = Type{ kind = .void } .return_stmt: { state.cells[usize(statement)].role = .return_stmt state.cells[usize(statement)].type = Type{ kind = .void } @@ -2335,6 +2336,7 @@ validate_call proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | dia BlockFlow :: struct { falls bool = true yields bool = false + breaks bool = false } @hide:file @@ -2367,6 +2369,16 @@ expression_falls_through proc(state @State, id ast.NodeId) bool { } } +@hide:file +expression_breaks proc(state @State, id ast.NodeId) bool { + tree :: state.tree + if (ast.node_class(tree, id) == .block) return block_flow(state, id).breaks + for 0..ast.child_count(tree, id) |i| { + if (expression_breaks(state, ast.child(tree, id, i))) return true + } + return false +} + @hide:file block_flow proc(state @State, id ast.NodeId) BlockFlow { tree :: state.tree @@ -2381,30 +2393,62 @@ block_flow proc(state @State, id ast.NodeId) BlockFlow { } match kind { .yield_stmt: { + if (ast.yield_stmt(tree, statement).value) |value| + result.breaks = result.breaks or expression_breaks(state, value) result.yields = true result.falls = false } - .return_stmt, .break_stmt, .continue_stmt, .invalid: result.falls = false + .return_stmt: { + if (ast.return_stmt(tree, statement).value) |value| + result.breaks = result.breaks or expression_breaks(state, value) + result.falls = false + } + .break_stmt: { + result.breaks = true + result.falls = false + } + .continue_stmt, .unreachable_stmt, .invalid: result.falls = false .block_stmt: { nested :: block_flow(state, ast.statement_block(tree, statement)) result.yields = result.yields or nested.yields + result.breaks = result.breaks or nested.breaks result.falls = nested.falls } .if_stmt: { conditional :: ast.if_stmt(tree, statement) then_flow :: block_flow(state, conditional.then_block) result.yields = result.yields or then_flow.yields + result.breaks = result.breaks or then_flow.breaks or expression_breaks(state, conditional.condition) if (conditional.else_block) |other| { else_flow :: block_flow(state, other) result.yields = result.yields or else_flow.yields + result.breaks = result.breaks or else_flow.breaks result.falls = then_flow.falls or else_flow.falls } } - .while_stmt: result.yields = result.yields or block_flow(state, ast.while_stmt(tree, statement).body).yields - .for_stmt: result.yields = result.yields or block_flow(state, ast.for_stmt(tree, statement).body).yields - .expression: result.falls = expression_falls_through(state, source) - .declaration: result.falls = expression_falls_through(state, ast.statement_decl(tree, statement).initializer) - .assignment_stmt: result.falls = expression_falls_through(state, ast.assignment_stmt(tree, statement).value) + .while_stmt: { + loop :: ast.while_stmt(tree, statement) + body :: block_flow(state, loop.body) + result.yields = result.yields or body.yields + result.breaks = result.breaks or expression_breaks(state, loop.condition) + if (loop.capture == null and ast.expr_kind(tree, loop.condition) == .literal_bool + and mem.eql(ast.literal_text(tree, loop.condition), "true")) + result.falls = body.breaks + } + .for_stmt: { + loop :: ast.for_stmt(tree, statement) + result.yields = result.yields or block_flow(state, loop.body).yields + result.breaks = result.breaks or expression_breaks(state, loop.iterable) + } + .expression, .declaration, .assignment_stmt: { + value :: if (kind == .expression) source + else if (kind == .declaration) ast.statement_decl(tree, statement).initializer + else ast.assignment_stmt(tree, statement).value + result.breaks = result.breaks or expression_breaks(state, value) + if (kind == .assignment_stmt) + result.breaks = result.breaks or expression_breaks(state, ast.assignment_stmt(tree, statement).target) + result.falls = expression_falls_through(state, value) + } else: {} } if (!result.falls) return result diff --git a/source/sema/check/check.test.hon b/source/sema/check/check.test.hon index bc2e8bd..6fa2b9f 100644 --- a/source/sema/check/check.test.hon +++ b/source/sema/check/check.test.hon @@ -684,7 +684,7 @@ return_errors_preserve_identity_check_unreachable_code_and_suppress_missing_retu `void_value proc() void { return 1 } `unknown proc() int { return absent } `syntax proc() int { return (1 } - `unreachable proc() int { + `after_return proc() int { ` return 7 ` -"bad" `} @@ -910,7 +910,7 @@ analyzes_return_paths_after_trap_propagation_without_poisoning_signatures test { `syntax_trap proc() int { ` return 1 stray `} - `unreachable proc() int { + `after_return proc() int { ` return 1 ` -"bad" `} @@ -1198,7 +1198,7 @@ while_checks_nested_bodies_without_claiming_a_zero_iteration_return test { diagnostic_store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&diagnostic_store) source :: - `missing proc() int { while true { return 1 } } + `terminal proc() int { while true { return 1 } } `bad_body proc() int { while false { -"bad" } } `continues proc() int { ` while false { -"bad" } @@ -1226,12 +1226,11 @@ while_checks_nested_bodies_without_claiming_a_zero_iteration_return test { result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) defer deinit(&result) - for usize(0)..usize(2) |index| { - function :: ast.package_decl(&tree, index) - body :: ast.function_decl(&tree, function).body - _ = try expect_fallthrough(&result, &diagnostic_store, function, ast.node_span(&tree, body)) - try expect_kind(&result, ast.block_statement(&tree, body, 0), .void) - } + terminal :: ast.package_decl(&tree, 0) + try testing.expect(fallthrough_diagnostic(&result, terminal) == null) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body + _ = try expect_fallthrough(&result, &diagnostic_store, ast.package_decl(&tree, 1), ast.node_span(&tree, body)) + try expect_kind(&result, ast.block_statement(&tree, body, 0), .void) continues_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body loop_statement :: ast.block_statement(&tree, continues_body, 0) loop :: ast.while_stmt(&tree, loop_statement) @@ -1261,7 +1260,7 @@ while_checks_nested_bodies_without_claiming_a_zero_iteration_return test { try expect_kind(&result, assignment.target, .int) try expect_kind(&result, assignment.value, .int) } - try testing.expect_equal(2, code_count(&diagnostic_store, .missing_return)) + try testing.expect_equal(1, code_count(&diagnostic_store, .missing_return)) try testing.expect_equal(1, code_count(&diagnostic_store, .condition_not_bool)) try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand)) } @@ -1518,7 +1517,7 @@ short_circuit_skip_paths_do_not_guarantee_a_return test { `call proc() int { take(true or 1) } `conditional proc() int { if false and missing_condition { return 1 } } `loop proc() int { while true or missing_guard { return 1 } } - `loop_body proc() int { while true { true or 1 } } + `loop_body proc(flag bool) int { while flag { true or 1 } } `arms proc(flag bool) int { ` if flag { false and missing_arm } else { return 1 } `} diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index dcdbf71..b492fd4 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -412,7 +412,7 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value) } .yield_stmt: try resolve_expr(state, scope, ast.yield_stmt(tree, statement).value?) - .break_stmt, .continue_stmt: {} + .break_stmt, .continue_stmt, .unreachable_stmt: {} .block_stmt: try resolve_arm(state, scope, ast.statement_block(tree, statement)) .defer_stmt, .errdefer_stmt: { cleanup :: ast.cleanup_stmt(tree, statement) diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 98f3a3e..28872f4 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -134,7 +134,8 @@ elaborate proc( if (class == .expression and ast.expr_kind(&original, id) == .builtin_call) { name :: ast.builtin_call(&original, id).name text :: ast.source_text(&original, ast.node_span(&original, name)) - if (mem.eql(text, "field") or mem.eql(text, "struct_type") or mem.eql(text, "typeinfo")) needed = true + if (mem.eql(text, "field") or mem.eql(text, "struct_type") or mem.eql(text, "typeinfo") + or mem.eql(text, "compile_error")) needed = true } if (class == .function and ast.function_generic(&original, id)) needed = true if (class == .function) { @@ -237,6 +238,35 @@ reject proc(state @mut State, id ast.NodeId, code diagnostics.Code) void ! Error state.bindings.items[usize(id)] = sema.Binding{ kind = .invalid, data = sema.BindingData{ diagnostic = diagnostic } } } +@hide:file +expand_compile_error proc(state @mut State, id ast.NodeId) void ! Error { + if (ast.call_arg_count(&state.tree, id) != 1) { + try reject(state, id, .invalid_compile_error) + return + } + state.evaluation_diagnostic = diagnostics.no_diagnostic + value :: try evaluate(state, ast.call_arg(&state.tree, id, 0)) + if (value.kind != .string) { + if (state.evaluation_diagnostic != diagnostics.no_diagnostic) try reject_evaluation(state, id) + else try reject(state, id, .invalid_compile_error) + return + } + text :: ast.literal_text(&state.tree, value.node) + bytes := arraylist.init(u8, state.allocator) + defer arraylist.deinit(&bytes) + cursor := usize(1) + while (cursor < text.len - 1) { + byte :: ast.string_next(text, &cursor) catch { + try reject(state, id, .invalid_escape) + return + } + try arraylist.append(&bytes, byte) + } + state.evaluation_diagnostic = try diagnostics.add_message(state.diagnostics, + .compile_error, ast.node_span(&state.tree, id), bytes.items) + try reject_evaluation(state, id) +} + @hide:file expand proc(state @mut State, id ast.NodeId) void ! Error { try refresh(state) @@ -287,6 +317,15 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { if (class == .statement and ast.stmt_kind(&state.tree, id) == .for_stmt) { loop :: ast.for_stmt(&state.tree, id) try expand(state, loop.iterable) + if (loop.index) |capture| { + annotation :: try builtin(state, id, .usize) + hashmap.put(&state.capture_types, capture, annotation) catch |err| { + match err { + .key_exists: {} + else: return err + } + } + } if (!loop.pointer_capture and loop.capture != null) { iterable :: try expression_type(state, loop.iterable, 0) element := no_node @@ -417,6 +456,10 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { } if (class == .expression and ast.expr_kind(&state.tree, id) == .builtin_call) { name :: ast.builtin_call(&state.tree, id).name + if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "compile_error")) { + try expand_compile_error(state, id) + return + } if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "field")) { try expand_field(state, id) return @@ -496,7 +539,7 @@ value_block_terminal proc(state @State, id ast.NodeId) bool { } if (class != .statement) return false match ast.stmt_kind(&state.tree, id) { - .yield_stmt, .return_stmt, .break_stmt, .continue_stmt: return true + .yield_stmt, .return_stmt, .break_stmt, .continue_stmt, .unreachable_stmt: return true .block_stmt: return value_block_terminal(state, ast.statement_block(&state.tree, id)) .if_stmt: { view :: ast.if_stmt(&state.tree, id) @@ -2300,7 +2343,11 @@ evaluate_ir_body proc(state @mut State, at, body, result_type ast.NodeId) Value diagnostic_store := diagnostics.init(state.allocator) defer diagnostics.deinit(&diagnostic_store) # Resolution's existing diagnostic IDs remain valid in this temporary store. - for (state.diagnostics.list.items) |diagnostic| try arraylist.append(&diagnostic_store.list, diagnostic) + for (state.diagnostics.list.items) |diagnostic| { + if (diagnostic.custom_message) |text| { + _ = try diagnostics.add_message(&diagnostic_store, diagnostic.code, diagnostic.span, text) + } else _ = try diagnostics.add(&diagnostic_store, diagnostic.code, diagnostic.span) + } original_diagnostics :: diagnostics.count(&diagnostic_store) checked := try typecheck.check(state.allocator, &tree, state.resolution, &diagnostic_store) defer typecheck.deinit(&checked) @@ -2744,6 +2791,7 @@ inline_statement proc(state @mut State, id ast.NodeId) TypeFlow ! Error { kind :: ast.stmt_kind(&state.tree, id) if (kind == .break_stmt) return .break if (kind == .continue_stmt) return .continue + if (kind == .unreachable_stmt) return .returned if (kind == .yield_stmt or kind == .return_stmt) { try expand(state, id) return if (kind == .yield_stmt) TypeFlow.yielded else TypeFlow.returned @@ -3911,6 +3959,11 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat @hide:file execute_type_statement_body proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { match ast.stmt_kind(&state.tree, statement) { + .unreachable_stmt: { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .unreachable_executed, ast.node_span(&state.tree, statement)) + return .invalid + } .defer_stmt: {} .errdefer_stmt: { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, @@ -4465,6 +4518,18 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error { view :: ast.unary_expr(&state.tree, id) if (view.operator == .bit_not) return try evaluate_ir(state, id, try expression_type(state, id, 0)) + if (view.operator == .unwrap) { + # source-known callbacks already own declaration identities, not interpreter handles. + value := try evaluate(state, view.operand) + annotation :: try expression_type(state, view.operand, 0) + if (value.kind == .function and annotation != no_node + and ast.expr_kind(&state.tree, annotation) == .optional_type) { + try arraylist.append(&state.rewrites.constraints, id) + value.annotation = ast.type_operand(&state.tree, annotation) + return value + } + return try evaluate_ir(state, id, try expression_type(state, id, 0)) + } value := try evaluate(state, view.operand) if (view.operator == .negate and value.kind == .integer) value.negative = value.magnitude != 0 and !value.negative else if (view.operator == .negate and value.kind == .real) value.magnitude = value.magnitude xor (u64(1) << 63) @@ -4917,6 +4982,8 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! return try builtin(state, id, .usize) if ((mem.eql(name, "maxval") or mem.eql(name, "minval")) and count == 1) return try canonical_type(state, ast.call_arg(&state.tree, id, 0)) + if ((mem.eql(name, "ptrcast") or mem.eql(name, "constcast")) and count == 2) + return try canonical_type(state, ast.call_arg(&state.tree, id, 0)) if (mem.eql(name, "divtrunc") and count == 2) { left :: ast.call_arg(&state.tree, id, 0) right :: ast.call_arg(&state.tree, id, 1) @@ -4974,6 +5041,7 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! if (destination.kind == .builtin) return callee if (destination.kind == .declaration and ast.node_class(&state.tree, destination.data.declaration) == .function) return try canonical_type(state, ast.function_decl(&state.tree, destination.data.declaration).return_type) signature :: try expression_type(state, callee, depth + 1) + if (signature != no_node and is_type(state, signature)) return try canonical_type(state, callee) if (signature != no_node and ast.expr_kind(&state.tree, signature) == .function_type) return try canonical_type(state, ast.callable_result(&state.tree, signature)) return no_node @@ -5121,8 +5189,18 @@ infer_value proc(state @mut State, function, pattern ast.NodeId, actual Value, a @hide:file infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []mut Value, contextual bool) bool ! Error { resolved :: binding(state, pattern) - if (resolved.kind == .declaration and parameter_index(state, function, resolved.data.declaration) != no_index) - return try infer_value(state, function, pattern, Value{ kind = .type, node = actual }, arguments, contextual) + if (resolved.kind == .declaration) { + index :: parameter_index(state, function, resolved.data.declaration) + if (index != no_index) { + if (arguments[index].kind == .type) { + expected :: try canonical_type(state, arguments[index].node) + # a fixed slice type supplies borrowing/string context; ordinary + # argument checking owns element, mutability and sentinel validation. + if (expected != no_node and ast.expr_kind(&state.tree, expected) == .slice_type) return true + } + return try infer_value(state, function, pattern, Value{ kind = .type, node = actual }, arguments, contextual) + } + } # only normalize the actual type: the pattern may still contain unbound # parameters or a type-generator call whose arguments must be inferred. concrete :: try canonical_type(state, actual) diff --git a/std/hashmap/hashmap.hon b/std/hashmap/hashmap.hon index 16a3fc6..69d73c2 100644 --- a/std/hashmap/hashmap.hon +++ b/std/hashmap/hashmap.hon @@ -110,10 +110,10 @@ put proc( for old_entries |entry| { if (entry.hash == 0) continue - idx := entry.hash & (new_entries.len - 1) - while (new_entries[idx].hash != 0) idx = (idx + 1) & (new_entries.len - 1) + rehash_index := entry.hash & (new_entries.len - 1) + while (new_entries[rehash_index].hash != 0) rehash_index = (rehash_index + 1) & (new_entries.len - 1) - new_entries[idx] = entry + new_entries[rehash_index] = entry } map.entries = new_entries @@ -176,7 +176,7 @@ str_hash proc(key []u8) usize { prime u32 :: 16777619 for key |byte| { - product u64 :: u64(hash xor u32(byte)) * prime + product u64 :: u64(hash xor u32(byte)) * u64(prime) hash = u32(product & u64(maxval!(u32))) } diff --git a/tests/native.py b/tests/native.py index fec7109..a843b59 100644 --- a/tests/native.py +++ b/tests/native.py @@ -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__":