From c65f41db4385dcfc2180c389b9f3a956694d8017 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 30 Oct 2026 12:53:11 +0100 Subject: [PATCH] feat: infer generic arguments from expected results --- README.md | 6 +- TODO.md | 8 +- source/compilation/compilation.test.hon | 5 +- source/diagnostics/diagnostics.hon | 2 +- source/main.hon | 1 + source/sema/specialize/specialize.hon | 343 ++++++++++++++++++++- source/sema/specialize/specialize.test.hon | 130 +++++++- tests/native.py | 168 +++++++++- 8 files changed, 626 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 1183d0c..827a8ca 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,9 @@ the parser supports arithmetic expressions with nested parenthesized groups, suc function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow newlines and a trailing comma; outside parentheses, newlines remain statement boundaries. type checking validates callable values, argument counts and argument types. -generic procedures mark compile-time parameters with `$`, such as `identity proc($T type, value T) T`. pass all parameters explicitly (`identity(i16, 42)`) or omit every compile-time argument when runtime argument types determine them (`identity(value)`). inference follows pointers, arrays, slices, optionals and specialized record constructors, including array counts; conflicting or undetermined arguments are diagnosed. specializations are cached by declaration and compile-time arguments, retain nominal generated-record identity, and erase compile-time parameters from runtime calls. +generic procedures mark compile-time parameters with `$`, such as `identity proc($T type, value T) T`. pass all parameters explicitly (`identity(i16, 42)`) or omit every compile-time argument when runtime argument types or an expected result type determine them (`identity(value)`). inference follows pointers, arrays, slices, optionals, fallible channels, callback signatures and specialized record constructors, including array counts; conflicting or undetermined arguments are diagnosed. specializations are cached by declaration and compile-time arguments, retain nominal generated-record identity, and erase compile-time parameters from runtime calls. + +expected result types flow from typed bindings, assignments, returns, ordinary call parameters, aggregate fields/elements, defaults, match arms, value-block yields and cleanup handlers. they also reach explicit `$()` expressions, `some!` payloads and arithmetic/bitwise operands; shift counts keep their independent types. rigid runtime arguments constrain inference before result context and contextual literal defaults; result context also types nested generic calls. `answer proc($T type) T { return 42 }` can therefore be called as `value i16 :: answer()`. optional presence and nominal generated types remain exact, explicit arguments are never overridden, and type-producing procedures still require every input explicitly. ```hon Fixed proc($T type, $N usize) type { @@ -196,7 +198,7 @@ native external declarations use `malloc c_func(size c_ulong) ?*mut anyopaque`, 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. +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. a declared result supplies the initializer parameters: `map hashmap.AutoHashMap(i16, i16) := hashmap.init(allocator)`; `StringHashMap` and custom `HashMap` results also infer their exact private/custom callbacks. explicit calls such as `hashmap.init(i16, i16, null, null, allocator)` remain supported. 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. diff --git a/TODO.md b/TODO.md index 810d8b2..c255e46 100644 --- a/TODO.md +++ b/TODO.md @@ -59,7 +59,8 @@ 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. +and custom callbacks with the real C allocator. expected result types infer initializer +types/callbacks, including automatic, string and custom maps. `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 @@ -139,8 +140,9 @@ 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. - - [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. + - [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 is covered below. + - [x] expected-result inference checkpoint: omitted compile-time type/value arguments infer from rigid runtime arguments and expected results before contextual literal defaults. typed bindings, assignments, returns, ordinary/nested calls, aggregate fields/elements/defaults, optional/fallible channels, callbacks, match arms, yielding/catch blocks, arithmetic/bitwise operands and explicit `$()` expressions retain exact checked types through interpreter/native execution. generated nominal results infer counts and private/custom callback identities; existing `hashmap.init(allocator)` initializes `AutoHashMap`, `StringHashMap` and custom `HashMap` without replacement library code. rigid conflicts, unrelated nominal generators and unresolved arguments diagnose; explicit calls and fully explicit type-producer inputs retain their contracts. owned specializations and repeat compilation are verified. + - [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including untagged unions and storage-bearing values), 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/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 3ef532a..f8145a8 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -81,6 +81,7 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` if (T == i16) return value else return value.missing `} `Box proc(T type) type { return struct { value T } } + `make_box proc($T type) Box(T) { return { value = 40 } } `count proc() usize { ` result := 0 ` while (result < 2) result = result + 1 @@ -267,7 +268,9 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { `main proc() i16 { ` if $(i16 != i16) { return $(i8(128)) } ` values :: $(data()) - ` box Box(i16) :: { value = identity(i16, values[0]) } + ` box Box(i16) :: make_box() + ` inferred i16 :: identity(40) + ` if (inferred != box.value) return 0 ` fixed Fixed(count()) :: { values = [box.value, 2] } ` same Fixed(2) :: fixed ` packet :: $(packet_data()) diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 5b8f77d..7b76857 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -287,7 +287,7 @@ messages :: $enummap.init(Code, []u8, { errdefer_outside_fallible = "errdefer requires an enclosing fallible function", cleanup_control_flow = "cleanup cannot transfer control out of itself", comptime_argument_required = "generic argument must be a compile-time value", - cannot_infer_generic = "cannot infer consistent compile-time arguments from runtime arguments", + cannot_infer_generic = "cannot infer consistent compile-time arguments from arguments and result context", specialization_cycle = "compile-time specialization depends on its unfinished result", specialization_limit = "compile-time specialization exceeds its recursion or instance limit", comptime_execution_limit = "compile-time execution exceeds its recursion or step limit", diff --git a/source/main.hon b/source/main.hon index fd64c4a..6197ffc 100644 --- a/source/main.hon +++ b/source/main.hon @@ -28,6 +28,7 @@ test import "@source/ast" test import "@source/parser" test import "@source/sema" test import "@source/sema/check" +test import "@source/sema/specialize" test import "@source/ir" test import "@source/lower" test import "@source/interpreter" diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 28872f4..caf432c 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -100,6 +100,8 @@ State :: struct { type_execution_depth usize = 0 type_execution_remaining usize = 0 value_expansion_depth usize = 0 + return_type ast.NodeId = no_node + yield_type ast.NodeId = no_node } #! elaborates templates into cached concrete declarations in the caller-owned `builder` arena. @@ -269,13 +271,66 @@ expand_compile_error proc(state @mut State, id ast.NodeId) void ! Error { @hide:file expand proc(state @mut State, id ast.NodeId) void ! Error { + try expand_expected(state, id, no_node) +} + +@hide:file +expand_expected proc(state @mut State, id, expected ast.NodeId) void ! Error { try refresh(state) if (state.expanded.items[usize(id)] != 0) return state.expanded.items[usize(id)] = 1 class :: ast.node_class(&state.tree, id) + saved_return :: state.return_type + saved_yield :: state.yield_type + defer { + state.return_type = saved_return + state.yield_type = saved_yield + } + if (class == .function) { + state.return_type = ast.function_decl(&state.tree, id).return_type + state.yield_type = no_node + } + if (class == .statement) { + match ast.stmt_kind(&state.tree, id) { + .declaration: { + view :: ast.statement_decl(&state.tree, id) + annotation := no_node + if (view.type) |type| { + try expand(state, type) + annotation = type + } + try expand_expected(state, view.initializer, annotation) + return + } + .assignment_stmt: { + view :: ast.assignment_stmt(&state.tree, id) + try expand(state, view.target) + try expand_expected(state, view.value, try expression_type(state, view.target, 0)) + return + } + .return_stmt, .yield_stmt: { + yielding :: ast.stmt_kind(&state.tree, id) == .yield_stmt + value :: if (yielding) ast.yield_stmt(&state.tree, id).value else ast.return_stmt(&state.tree, id).value + annotation :: if (yielding) state.yield_type else state.return_type + if (value) |operand| try expand_expected(state, operand, annotation) + return + } + else: {} + } + } + if (class == .field) { + view :: ast.field(&state.tree, id) + annotation := no_node + if (view.type) |type| { + try expand(state, type) + annotation = type + } + if (view.value) |value| try expand_expected(state, value, annotation) + return + } if (class == .expression and ast.expr_kind(&state.tree, id) == .block_expr) { body :: ast.block_expr(&state.tree, id) - annotation :: try value_block_type(state, body, 0) + annotation :: if (expected != no_node) expected else try value_block_type(state, body, 0) if (is_type(state, annotation) or type_record_storage(state, annotation, 0)) { selected :: try evaluate_type_value_block(state, id, annotation) if (selected == no_node) { @@ -289,6 +344,7 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { } state.value_expansion_depth += 1 defer state.value_expansion_depth -= 1 + state.yield_type = annotation try expand(state, body) return } @@ -374,8 +430,8 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { } if (class == .expression and ast.expr_kind(&state.tree, id) == .catch_expr) { expression :: ast.catch_expr(&state.tree, id) + try expand_expected(state, expression.operand, expected) if (expression.capture) |capture| { - try expand(state, expression.operand) annotation :: try expression_type(state, expression.operand, 0) if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .fallible_type) { hashmap.put(&state.capture_types, capture, ast.binary_expr(&state.tree, annotation).right) catch |err| { @@ -386,17 +442,21 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { } } } + annotation :: if (expected != no_node) expected else try expression_type(state, id, 0) + state.yield_type = annotation + try expand_expected(state, expression.handler, annotation) + return } if (class == .statement and ast.stmt_kind(&state.tree, id) == .expression) { expression :: ast.statement_expr(&state.tree, id) if ( ast.expr_kind(&state.tree, expression) == .match_expr - and try specialize_match(state, expression, id) + and try specialize_match(state, expression, id, no_node) ) return } if ( class == .expression and ast.expr_kind(&state.tree, id) == .match_expr - and try specialize_match(state, id, no_node) + and try specialize_match(state, id, no_node, expected) ) return if (class == .statement and ast.stmt_kind(&state.tree, id) == .if_stmt) { conditional :: ast.if_stmt(&state.tree, id) @@ -435,6 +495,7 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { } if (class == .expression and ast.expr_kind(&state.tree, id) == .comptime_expr) { operand :: ast.comptime_operand(&state.tree, id) + try expand_expected(state, operand, expected) state.evaluation_diagnostic = diagnostics.no_diagnostic value :: try evaluate(state, operand) if (value.annotation == no_node or !type_record_storage(state, value.annotation, 0)) @@ -456,6 +517,18 @@ 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)), "some") + and expected != no_node and ast.call_arg_count(&state.tree, id) == 1 + ) { + annotation := try canonical_type(state, expected) + if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .fallible_type) + annotation = try canonical_type(state, ast.binary_expr(&state.tree, annotation).left) + if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .optional_type) { + try expand_expected(state, ast.call_arg(&state.tree, id, 0), ast.type_operand(&state.tree, annotation)) + return + } + } if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "compile_error")) { try expand_compile_error(state, id) return @@ -482,9 +555,112 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { and ast.node_class(&state.tree, resolved.data.declaration) == .function and template(state, resolved.data.declaration) ) { - try specialize_call(state, id, resolved.data.declaration) + try specialize_call(state, id, resolved.data.declaration, expected) return } + signature := no_node + if (resolved.kind == .declaration and ast.node_class(&state.tree, resolved.data.declaration) == .function) + signature = resolved.data.declaration + else signature = try expression_type(state, callee, 0) + if ( + signature != no_node + and (ast.node_class(&state.tree, signature) == .function + or ast.expr_kind(&state.tree, signature) == .function_type) + and ast.function_param_count(&state.tree, signature) == ast.call_arg_count(&state.tree, id) + ) { + for (0..ast.call_arg_count(&state.tree, id)) |i| { + annotation :: ast.param(&state.tree, ast.function_param(&state.tree, signature, i)).type + try expand_expected(state, ast.call_arg(&state.tree, id, i), annotation) + } + return + } + } + if (class == .expression and expected != no_node) { + kind :: ast.expr_kind(&state.tree, id) + if (kind == .unary) { + view :: ast.unary_expr(&state.tree, id) + if (view.operator == .negate or view.operator == .bit_not or view.operator == .logical_not) { + try expand_expected(state, view.operand, expected) + return + } + } + if (kind == .binary and !is_type(state, expected)) { + view :: ast.binary_expr(&state.tree, id) + match view.operator { + .add, .subtract, .multiply, .divide, .bit_and, .bit_or, .bit_xor, + .logical_and, .logical_or: { + try expand_expected(state, view.left, expected) + try expand_expected(state, view.right, expected) + return + } + .shift_left, .shift_right, .shift_left_saturating: { + try expand_expected(state, view.left, expected) + try expand(state, view.right) + return + } + else: {} + } + } + if (kind == .try_expr) { + try expand_expected(state, ast.type_operand(&state.tree, id), expected) + return + } + if (kind == .match_expr) { + try expand(state, ast.match_expr(&state.tree, id).subject) + for (0..ast.match_arm_count(&state.tree, id)) |i| { + arm_id :: ast.match_arm(&state.tree, id, i) + arm :: ast.match_arm_view(&state.tree, arm_id) + for (0..ast.match_pattern_count(&state.tree, arm_id)) |j| + try expand(state, ast.match_pattern(&state.tree, arm_id, j)) + state.yield_type = expected + annotation :: if (arm.expression) expected else no_node + try expand_expected(state, arm.body, annotation) + } + return + } + } + if (class == .expression) { + kind :: ast.expr_kind(&state.tree, id) + if (kind == .record_literal or kind == .array_literal) { + annotation := expected + if (kind == .record_literal) { + if (ast.record_literal(&state.tree, id).type) |type| { + try expand(state, type) + annotation = type + } + } + shape := if (annotation != no_node) try canonical_type(state, annotation) else no_node + if (shape != no_node and ast.expr_kind(&state.tree, shape) == .fallible_type) + shape = try canonical_type(state, ast.binary_expr(&state.tree, shape).left) + if (shape != no_node and ast.expr_kind(&state.tree, shape) == .optional_type) + shape = try canonical_type(state, ast.type_operand(&state.tree, shape)) + if ( + kind == .array_literal and shape != no_node + and (ast.expr_kind(&state.tree, shape) == .array_type or ast.expr_kind(&state.tree, shape) == .slice_type) + ) { + element :: if (ast.expr_kind(&state.tree, shape) == .array_type) + ast.array_type(&state.tree, shape).element else ast.slice_type(&state.tree, shape).element + for (0..ast.array_element_count(&state.tree, id)) |i| + try expand_expected(state, ast.array_element(&state.tree, id, i), element) + return + } + if ( + kind == .record_literal and shape != no_node + and (ast.expr_kind(&state.tree, shape) == .record_type or ast.expr_kind(&state.tree, shape) == .union_type) + ) { + for (0..ast.record_field_count(&state.tree, id)) |i| { + field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i)) + slot :: if (field.name != null) type_record_slot(state, shape, field.name?) else i + field_type := no_node + if (slot < ast.record_field_count(&state.tree, shape)) { + if (ast.field(&state.tree, ast.record_field(&state.tree, shape, slot)).type) |type| + field_type = type + } + if (field.value) |value| try expand_expected(state, value, field_type) + } + return + } + } } count :: ast.child_count(&state.tree, id) for (0..count) |i| { @@ -1436,7 +1612,7 @@ expand_inline_match proc(state @mut State, id, subject_type ast.NodeId) void ! E # runtime bodies share the type-producer selector, but retain ordinary runtime # parameters and control transfers in the selected arm. @hide:file -specialize_match proc(state @mut State, id, statement ast.NodeId) bool ! Error { +specialize_match proc(state @mut State, id, statement, expected ast.NodeId) bool ! Error { explicit :: ast.expr_kind(&state.tree, ast.match_expr(&state.tree, id).subject) == .comptime_expr try expand_inline_match(state, id, no_node) if (ast.expr_kind(&state.tree, id) != .match_expr) return true @@ -1494,7 +1670,7 @@ specialize_match proc(state @mut State, id, statement ast.NodeId) bool ! Error { try reject_evaluation(state, id) return true } - try expand(state, selected) + try expand_expected(state, selected, expected) if (statement != no_node) { replacement :: try ast.add_expression_stmt(state.builder, selected) try replace(state, statement, replacement) @@ -1632,7 +1808,48 @@ compiletime_result proc(state @State, annotation ast.NodeId) bool { } @hide:file -specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { +inference_contextual proc(state @State, id ast.NodeId) bool { + kind :: ast.expr_kind(&state.tree, id) + if (kind == .literal_null or kind == .literal_string) return true + if (ast.expr_kind(&state.tree, id) == .record_literal) { + annotation :: ast.record_literal(&state.tree, id).type + return annotation == null or ast.expr_kind(&state.tree, annotation?) == .enum_literal + } + return flexible(state, id) or contextual_literal(state, id) +} + +@hide:file +inference_phase proc(state @State, id ast.NodeId) usize { + if (inference_contextual(state, id)) return 2 + if (ast.expr_kind(&state.tree, id) == .call) { + callee :: binding(state, ast.call_expr(&state.tree, id).callee) + if ( + callee.kind == .declaration and ast.node_class(&state.tree, callee.data.declaration) == .function + and template(state, callee.data.declaration) + and ast.call_arg_count(&state.tree, id) != ast.function_param_count(&state.tree, callee.data.declaration) + ) return 1 + } + return 0 +} + +@hide:file +annotation_ready proc( + state @State, function, id ast.NodeId, arguments []Value, depth usize, +) bool { + if (depth >= 128) return false + resolved :: binding(state, id) + if (resolved.kind == .declaration) { + index :: parameter_index(state, function, resolved.data.declaration) + if (index != no_index) return arguments[index].kind != .invalid + } + for (0..ast.child_count(&state.tree, id)) |i| { + if (!annotation_ready(state, function, ast.child(&state.tree, id, i), arguments, depth + 1)) return false + } + return true +} + +@hide:file +specialize_call proc(state @mut State, call, function, expected ast.NodeId) void ! Error { if (state.depth >= 128 or state.instances.items.len >= 4096) { try reject(state, call, .specialization_limit) return @@ -1664,8 +1881,9 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { if (view.comptime and !explicit) continue argument :: ast.call_arg(&state.tree, call, position) position += 1 - try expand(state, argument) if (type_result or view.comptime) { + context_annotation :: try argument_annotation(state, function, view.type, arguments) + try expand_expected(state, argument, context_annotation) state.evaluation_diagnostic = diagnostics.no_diagnostic annotation := view.type contextual := contextual_literal(state, argument) @@ -1689,7 +1907,16 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { } } if (!explicit) { - for (0..usize(2)) |phase| { + # rigid arguments constrain first; result context then types nested generic + # calls before flexible literals choose their default widths. + for (0..usize(3)) |phase| { + if (phase == 1 and expected != no_node) { + result :: ast.function_decl(&state.tree, function).return_type + if (!try infer_result(state, function, result, expected, arguments)) { + try reject(state, call, .cannot_infer_generic) + return + } + } position = 0 for (0..count) |i| { parameter :: ast.function_param(&state.tree, function, i) @@ -1697,12 +1924,12 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { if (view.comptime) continue argument :: runtime_arguments.items[position] position += 1 - contextual := flexible(state, argument) or contextual_literal(state, argument) - if (ast.expr_kind(&state.tree, argument) == .record_literal) { - annotation :: ast.record_literal(&state.tree, argument).type - contextual = annotation == null or ast.expr_kind(&state.tree, annotation?) == .enum_literal + contextual :: inference_contextual(state, argument) + if (inference_phase(state, argument) != phase) continue + if (annotation_ready(state, function, view.type, arguments, 0)) { + annotation :: try argument_annotation(state, function, view.type, arguments) + try expand_expected(state, argument, annotation) } - if (contextual != (phase == 1)) continue actual :: try expression_type(state, argument, 0) # contextual literals need not have an independent type; the rewritten # call checks them against the substituted signature after inference. @@ -1723,6 +1950,16 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { return } } + position = 0 + for (0..count) |i| { + view :: ast.param(&state.tree, ast.function_param(&state.tree, function, i)) + if (view.comptime or type_result) continue + argument :: runtime_arguments.items[position] + position += 1 + if (state.expanded.items[usize(argument)] != 0) continue + annotation :: try argument_annotation(state, function, view.type, arguments) + try expand_expected(state, argument, annotation) + } key := usize(function) for arguments |argument| { if (argument.kind != .invalid) key = mix(key, try value_hash(state, argument)) @@ -2021,10 +2258,25 @@ contextual_literal proc(state @State, id ast.NodeId) bool { return kind == .record_literal or kind == .array_literal or kind == .enum_literal or kind == .block_expr } +@hide:file +annotation_generic proc(state @State, function, id ast.NodeId, depth usize) bool { + if (depth >= 128) return true + resolved :: binding(state, id) + if ( + resolved.kind == .declaration + and parameter_index(state, function, resolved.data.declaration) != no_index + ) return true + for (0..ast.child_count(&state.tree, id)) |i| { + if (annotation_generic(state, function, ast.child(&state.tree, id, i), depth + 1)) return true + } + return false +} + @hide:file argument_annotation proc( state @mut State, function, annotation ast.NodeId, arguments []Value, ) ast.NodeId ! Error { + if (!annotation_generic(state, function, annotation, 0)) return annotation mapping :: try mem.alloc(ast.NodeId, state.allocator, state.original_count) defer mem.free(state.allocator, mapping) mem.set(mapping, no_node) @@ -2057,6 +2309,7 @@ argument_annotation proc( @hide:file evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Error { + try expand_expected(state, id, annotation) if (!is_type(state, annotation) and type_record_storage(state, annotation, 0)) { if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 state.type_execution_depth += 1 @@ -2667,6 +2920,7 @@ type_snapshot_catch proc( view :: ast.catch_expr(&state.tree, id) operand :: try type_snapshot(state, execution, view.operand) if (operand == no_node) return no_node + try expand_expected(state, operand, annotation) operand_type :: try expression_type(state, operand, 0) if (operand_type == no_node or ast.expr_kind(&state.tree, operand_type) != .fallible_type) return no_node channels :: ast.binary_expr(&state.tree, operand_type) @@ -3707,7 +3961,7 @@ type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.No return concrete.node } if (!try expand_type_matches(state, expression)) return no_node - try expand(state, expression) + try expand_expected(state, expression, annotation) resolved :: binding(state, expression) if (resolved.kind == .invalid) { state.evaluation_diagnostic = resolved.data.diagnostic @@ -5233,6 +5487,25 @@ infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []m element :: if (actual_kind == .optional_type) ast.type_operand(&state.tree, concrete) else concrete return try infer(state, function, ast.type_operand(&state.tree, pattern), element, arguments, contextual) } + .fallible_type: { + if (actual_kind != .fallible_type) return false + p :: ast.binary_expr(&state.tree, pattern) + a :: ast.binary_expr(&state.tree, concrete) + return (try infer(state, function, p.left, a.left, arguments, contextual) + and try infer(state, function, p.right, a.right, arguments, contextual)) + } + .function_type: { + if (actual_kind != .function_type) return false + count :: ast.function_param_count(&state.tree, pattern) + if (count != ast.function_param_count(&state.tree, concrete)) return false + for (0..count) |i| { + p :: ast.param(&state.tree, ast.function_param(&state.tree, pattern, i)).type + a :: ast.param(&state.tree, ast.function_param(&state.tree, concrete, i)).type + if (!try infer(state, function, p, a, arguments, contextual)) return false + } + return try infer(state, function, ast.callable_result(&state.tree, pattern), + ast.callable_result(&state.tree, concrete), arguments, contextual) + } .call: { callee :: binding(state, ast.call_expr(&state.tree, pattern).callee) if (callee.kind != .declaration) return false @@ -5253,6 +5526,44 @@ infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []m } } +@hide:file +infer_result proc( + state @mut State, + function, pattern, actual ast.NodeId, + arguments []mut Value, +) bool ! Error { + concrete :: try canonical_type(state, actual) + if (concrete == no_node) return true + if ( + ast.expr_kind(&state.tree, concrete) == .optional_type + and ast.expr_kind(&state.tree, pattern) != .optional_type + ) { + resolved :: binding(state, pattern) + if (resolved.kind == .declaration) { + index :: parameter_index(state, function, resolved.data.declaration) + if (index != no_index) { + if (arguments[index].kind == .invalid) + return try infer(state, function, pattern, concrete, arguments, false) + if (arguments[index].kind == .type and try same_type(state, arguments[index].node, concrete)) return true + } + } + return try infer_result(state, function, pattern, ast.type_operand(&state.tree, concrete), arguments) + } + if ( + ast.expr_kind(&state.tree, concrete) == .fallible_type + and ast.expr_kind(&state.tree, pattern) != .fallible_type + ) return try infer_result(state, function, pattern, ast.binary_expr(&state.tree, concrete).left, arguments) + if (ast.expr_kind(&state.tree, pattern) == .fallible_type) { + view :: ast.binary_expr(&state.tree, pattern) + if (ast.expr_kind(&state.tree, concrete) != .fallible_type) + return try infer_result(state, function, view.left, concrete, arguments) + channels :: ast.binary_expr(&state.tree, concrete) + return (try infer_result(state, function, view.left, channels.left, arguments) + and try infer_result(state, function, view.right, channels.right, arguments)) + } + return try infer(state, function, pattern, concrete, arguments, false) +} + @hide:file materialize proc(state @mut State, value Value) Value ! Error { result := value diff --git a/source/sema/specialize/specialize.test.hon b/source/sema/specialize/specialize.test.hon index 48bb2bf..920f3d5 100644 --- a/source/sema/specialize/specialize.test.hon +++ b/source/sema/specialize/specialize.test.hon @@ -11,7 +11,7 @@ import "@source/parser" @hide:file expect_specialization proc( source []u8, expected diagnostics.Code, expected_count usize, -) void ! (mem.AllocError | diagnostics.CapacityError | strpool.InternError | testing.Error) { +) void ! (mem.AllocError | diagnostics.CapacityError | strpool.InternError | lexer.SourceError | testing.Error) { strings := strpool.init(mem.c_allocator) defer strpool.deinit(&strings) diagnostic_store := diagnostics.init(mem.c_allocator) @@ -57,7 +57,7 @@ infers_nested_nominal_pointer_parameters_and_readonly_slice_views test { `} `observe proc($T type, values []T) usize { return values.len } `reserve proc($T type, storage @mut Storage(T), count usize) void { - ` _ = observe(storage.items.ptr[..count]) + ` observe(storage.items.ptr[..count]) `} `append proc($T type, storage @mut Storage(T), value T) void { ` reserve(storage, storage.items.len) @@ -65,7 +65,7 @@ infers_nested_nominal_pointer_parameters_and_readonly_slice_views test { `} `release proc($T type, storage @mut Storage(T)) void { ` allocation :: storage.items.ptr[..storage.capacity] - ` _ = observe(allocation) + ` observe(allocation) `} `forward proc(storage @mut Storage(i16)) void { ` append(storage, 7) @@ -127,3 +127,127 @@ infers_integer_builtin_results_from_their_type_or_rigid_operand test { , .cannot_infer_generic, 0, ) } + +infers_direct_results_before_contextual_literal_defaults test { + try expect_specialization( + `make proc($T type) T { return T(7) } + `identity proc($T type, value T) T { return value } + `sized proc($N usize, values [N]i16) [N]i16 { return values } + `consume proc(value i16) i16 { return value } + `main proc() i16 { + ` value i16 := identity(1) + ` value = make() + ` row [2]i16 :: sized([2, 3]) + ` return consume(make()) + value + row[0] + `} + , .cannot_infer_generic, 0, + ) +} + +infers_type_and_value_arguments_from_generated_nominal_results test { + try expect_specialization( + `Fixed proc($T type, $N usize) type { + ` return struct { value T = 0, count usize = N } + `} + `make proc($T type, $N usize) Fixed(T, N) { return {} } + `identity proc($T type, $N usize, value T) Fixed(T, N) { + ` return { value = value } + `} + `main proc() i16 { + ` first Fixed(i16, 2) :: make() + ` second Fixed(i16, 3) :: identity(i16(7)) + ` cached Fixed(i16, 2) :: make() + ` return first.value + second.value + cached.value + `} + , .cannot_infer_generic, 0, + ) +} + +propagates_expected_results_through_nested_aggregates_and_success_channels test { + try expect_specialization( + `Error :: enum { failed } + `Record :: struct { value i16, rows [2]mut i16 } + `make proc($T type) T { return 7 } + `optional proc() ?i16 { return make() } + `fallible proc() i16 ! Error { return make() } + `main proc() Record { + ` record Record := { value = make(), rows = [make(), make()] } + ` record.value = make() + ` record.rows[0] = make() + ` maybe ?i16 :: make() + ` result i16 ! Error :: make() + ` return { value = make(), rows = [make(), make()] } + `} + , .cannot_infer_generic, 0, + ) +} + +propagates_expected_results_through_match_arms_and_value_block_yields test { + try expect_specialization( + `make proc($T type) T { return T(7) } + `choose proc(flag bool) i16 { + ` return match flag { + ` true: make(), + ` false: { yield make() }, + ` } + `} + `main proc() i16 { + ` value i16 :: { yield make() } + ` return value + choose(true) + `} + , .cannot_infer_generic, 0, + ) +} + +rejects_expected_results_that_conflict_with_rigid_runtime_arguments test { + try expect_specialization( + `identity proc($T type, value T) T { return value } + `main proc() i16 { return identity(u8(7)) } + , .cannot_infer_generic, 1, + ) + try expect_specialization( + `Box proc($T type) type { return struct { value T } } + `make proc($T type, value T) Box(T) { return { value = value } } + `main proc() Box(i16) { return make(u8(7)) } + , .cannot_infer_generic, 1, + ) +} + +rejects_unrelated_nominal_results_and_unresolved_compile_time_arguments test { + try expect_specialization( + `Box proc($T type) type { return struct { value T = 0 } } + `Other proc($T type) type { return struct { value T = 0 } } + `make proc($T type) Box(T) { return {} } + `main proc() Other(i16) { return make() } + , .cannot_infer_generic, 1, + ) + try expect_specialization( + `make proc($T type, $N usize) T { return T(7) } + `main proc() i16 { return make() } + , .cannot_infer_generic, 1, + ) + try expect_specialization( + `make proc($T type) T { return T(7) } + `main proc() void { make() } + , .cannot_infer_generic, 1, + ) +} + +preserves_explicit_arguments_and_explicit_type_procedure_inputs test { + try expect_specialization( + `identity proc($T type, value T) T { return value } + `main proc() i16 { return identity(u8, 7) } + , .return_type_mismatch, 1, + ) + try expect_specialization( + `Box proc($T type) type { return struct { value T = 0 } } + `make proc($T type) Box(T) { return {} } + `main proc() Box(i16) { return make(u8) } + , .return_type_mismatch, 1, + ) + try expect_specialization( + `Box proc($T type) type { return struct { value T } } + `main proc() void { value Box() :: { value = 7 } } + , .argument_count_mismatch, 1, + ) +} diff --git a/tests/native.py b/tests/native.py index a843b59..1321d45 100644 --- a/tests/native.py +++ b/tests/native.py @@ -998,6 +998,153 @@ CASES = { } """, b"42\n", None, ), + "expected_result_inference_direct_return_without_runtime_arguments": ( + """ + answer proc($T type) T { return 42 } + main proc() i16 { return answer() } + """, b"42\n", None, + ), + "expected_result_inference_assignment_return_and_nested_parameters": ( + """ + answer proc($T type) T { return 42 } + identity proc($T type, value T) T { return value } + read proc() i16 { return identity(42) } + consume proc(value i16) i16 { return value } + main proc() [4]i16 { + value i16 := identity(0) + value = answer() + return [value, read(), consume(identity(42)), consume(identity(answer()))] + } + """, b"[42, 42, 42, 42]\n", None, + ), + "expected_result_inference_nested_aggregate_fields_and_elements": ( + """ + identity proc($T type, value T) T { return value } + answer proc($T type) T { return 42 } + Inner :: struct { value i16 } + Outer :: struct { rows [2]Inner } + main proc() [2]i16 { + result Outer :: { rows = [{ value = identity(42) }, { value = answer() }] } + return [result.rows[0].value, result.rows[1].value] + } + """, b"[42, 42]\n", None, + ), + "expected_result_inference_precedes_contextual_literal_defaults": ( + """ + Key :: enum { first, last } + Record :: struct { value i16 } + identity proc($T type, value T) T { return value } + main proc() i16 { + number u8 :: identity(42) + key Key :: identity(.last) + record Record :: identity({ value = 42 }) + absent ?i16 :: identity(null) + text []u8 :: identity("ab") + if (number != 42 or key != .last or absent != null or text.len != 2) return -1 + return record.value + } + """, b"42\n", None, + ), + "expected_result_inference_generated_nominal_type_and_value_arguments": ( + """ + Box proc($T type, $count usize) type { + return struct { value T, length usize = count } + } + create proc($T type, $count usize, value T) Box(T, count) { + return { value = value } + } + main proc() i16 { + box Box(i16, 2) :: create(40) + explicit Box(i16, 2) :: create(i16, 2, 40) + if (box.value != explicit.value or box.length != 2) return -1 + return box.value + i16(box.length) + } + """, b"42\n", None, + ), + "expected_result_inference_optional_and_fallible_success_contexts": ( + """ + Error :: enum { failed } + identity proc($T type, value T) T { return value } + optional proc() ?i16 { return identity(42) } + fallible proc() i16 ! Error { return identity(42) } + main proc() [2]i16 { return [optional()?, fallible() catch 0] } + """, b"[42, 42]\n", None, + ), + "expected_result_inference_match_arms_and_value_block_yields": ( + """ + answer proc($T type) T { return 42 } + choose proc(flag bool) i16 { + return match flag { + true: answer(), + false: { yield answer() }, + } + } + main proc() [2]i16 { return [choose(true), choose(false)] } + """, b"[42, 42]\n", None, + ), + "expected_result_inference_inside_compile_time_type_producers": ( + """ + answer proc($T type) T { return 2 } + Fixed proc() type { + count usize :: answer() + return [count]i16 + } + main proc() Fixed() { return [19, 23] } + """, b"[19, 23]\n", None, + ), + "expected_result_inference_cleanup_comptime_and_expression_contexts": ( + """ + Error :: enum { failed } + answer proc($T type) T { return 21 } + fail proc($T type) T ! Error { return .failed } + recover proc() i16 { return fail() catch |error| { yield answer() } } + propagate proc() i16 ! Error { return try fail() } + Default :: struct { value i16 = answer() } + main proc() [7]i16 { + value ?i16 :: some!(answer()) + constant i16 :: $(answer()) + defaults Default :: {} + return [answer() + answer(), -answer(), ~answer(), fail() catch answer(), + recover(), (value?), constant + defaults.value] + } + """, b"[42, -21, -22, 21, 21, 21, 42]\n", None, + ), + "expected_result_inference_structural_returns_and_nested_optional_presence": ( + """ + Error :: enum { failed } + exact proc(value i16) i16 { return value } + callback proc($T type) @proc(value T) T { return exact } + optional proc($T type) ?T { return 42 } + fallible proc($T, $E type) T ! E { return 42 } + identity proc($T type, value T) T { return value } + main proc() [4]i16 { + call @proc(value i16) i16 :: callback() + maybe ?i16 :: optional() + result i16 ! Error :: fallible() + nested ??i16 :: identity(some!(null)) + if (nested == null or nested? != null) return [0, 0, 0, 0] + return [call(42), (maybe?), result catch 0, 42] + } + """, b"[42, 42, 42, 42]\n", None, + ), + "expected_result_inference_rejects_conflicting_rigid_argument": ( + """ + identity proc($T type, value T) T { return value } + main proc() u16 { + value i16 :: 42 + return identity(value) + } + """, b"", b"cannot infer consistent compile-time arguments", + ), + "expected_result_inference_requires_context_for_result_only_arguments": ( + """ + answer proc($T type) T { return 42 } + main proc() i16 { + value :: answer() + return 42 + } + """, b"", b"cannot infer consistent compile-time arguments", + ), "generic_contextual_enum_rejects_unknown_member": ( """ Key :: enum { first } @@ -9554,7 +9701,7 @@ def check_hashmap_packages(temporary, executable): 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) + map hashmap.HashMap(i16, Value, collision_hash, equivalent) := hashmap.init(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 @@ -9604,7 +9751,7 @@ def check_hashmap_packages(temporary, executable): return 42 } exercise_automatic_and_string_keys proc(allocator mem.Allocator) i16 ! (hashmap.PutError | mem.AllocError) { - automatic := hashmap.init(i16, Value, null, null, allocator) + automatic hashmap.AutoHashMap(i16, Value) := hashmap.init(allocator) defer hashmap.deinit(&automatic) for (i16(0)..i16(12)) |key| { try hashmap.put(&automatic, key, Value{ number = key, items = [key, key + 1] }) @@ -9619,7 +9766,7 @@ def check_hashmap_packages(temporary, executable): 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 } + strings hashmap.StringHashMap(Value) := hashmap.init(allocator) defer hashmap.deinit(&strings) left [3]mut u8 := [97, 0, 98] right [3]mut u8 := [97, 0, 98] @@ -9673,7 +9820,7 @@ def check_hashmap_packages(temporary, executable): 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) + map hashmap.AutoHashMap(K, usize) := hashmap.init(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) } @@ -9698,7 +9845,7 @@ def check_hashmap_packages(temporary, executable): 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) + flags hashmap.AutoHashMap(bool, i16) := hashmap.init(mem.c_allocator) defer hashmap.deinit(&flags) try hashmap.put(&flags, false, 19) try hashmap.put(&flags, true, 23) @@ -9706,14 +9853,14 @@ def check_hashmap_packages(temporary, executable): 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) + sparse hashmap.AutoHashMap(SparseAlias, i16) := hashmap.init(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) + ids hashmap.AutoHashMap(Id, i16) := hashmap.init(mem.c_allocator) defer hashmap.deinit(&ids) try hashmap.put(&ids, Id(Inner(0)), 19) try hashmap.put(&ids, Id(Inner(maxval!(u64))), 23) @@ -9734,15 +9881,14 @@ def check_hashmap_packages(temporary, executable): 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 } + strings hashmap.StringHashMap(i16) := hashmap.init(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) + records hashmap.AutoHashMap(Key, i16) := hashmap.init(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] }] } @@ -9774,7 +9920,7 @@ def check_hashmap_packages(temporary, executable): 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) + custom hashmap.HashMap(u16, i16, custom_hash, custom_eql) := hashmap.init(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| {