feat: infer generic arguments from expected results

This commit is contained in:
2026-10-30 12:53:11 +01:00
parent b1cc97cc77
commit c65f41db43
8 changed files with 626 additions and 37 deletions
+4 -2
View File
@@ -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. 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 ```hon
Fixed proc($T type, $N usize) type { 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. 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. 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.
+5 -3
View File
@@ -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, existing hash maps execute collision chains, rehashing, duplicate/absent lookups,
aggregate copies and allocator failure/cleanup through both paths; native checks aggregate copies and allocator failure/cleanup through both paths; native checks
cover automatic integer/bool/enum/distinct/array/record keys, content-based strings 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. `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. `compile_error!` retains owned user messages and skips unselected compile-time bodies.
boolean and optional-capture `if ... else` expressions reuse checked matches for 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] 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] 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] 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. - [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.
- [ ] 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] 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`. - [ ] 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. - [ ] 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. - [ ] 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.
+4 -1
View File
@@ -81,6 +81,7 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
` if (T == i16) return value else return value.missing ` if (T == i16) return value else return value.missing
`} `}
`Box proc(T type) type { return struct { value T } } `Box proc(T type) type { return struct { value T } }
`make_box proc($T type) Box(T) { return { value = 40 } }
`count proc() usize { `count proc() usize {
` result := 0 ` result := 0
` while (result < 2) result = result + 1 ` while (result < 2) result = result + 1
@@ -267,7 +268,9 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
`main proc() i16 { `main proc() i16 {
` if $(i16 != i16) { return $(i8(128)) } ` if $(i16 != i16) { return $(i8(128)) }
` values :: $(data()) ` 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] } ` fixed Fixed(count()) :: { values = [box.value, 2] }
` same Fixed(2) :: fixed ` same Fixed(2) :: fixed
` packet :: $(packet_data()) ` packet :: $(packet_data())
+1 -1
View File
@@ -287,7 +287,7 @@ messages :: $enummap.init(Code, []u8, {
errdefer_outside_fallible = "errdefer requires an enclosing fallible function", errdefer_outside_fallible = "errdefer requires an enclosing fallible function",
cleanup_control_flow = "cleanup cannot transfer control out of itself", cleanup_control_flow = "cleanup cannot transfer control out of itself",
comptime_argument_required = "generic argument must be a compile-time value", 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_cycle = "compile-time specialization depends on its unfinished result",
specialization_limit = "compile-time specialization exceeds its recursion or instance limit", specialization_limit = "compile-time specialization exceeds its recursion or instance limit",
comptime_execution_limit = "compile-time execution exceeds its recursion or step limit", comptime_execution_limit = "compile-time execution exceeds its recursion or step limit",
+1
View File
@@ -28,6 +28,7 @@ test import "@source/ast"
test import "@source/parser" test import "@source/parser"
test import "@source/sema" test import "@source/sema"
test import "@source/sema/check" test import "@source/sema/check"
test import "@source/sema/specialize"
test import "@source/ir" test import "@source/ir"
test import "@source/lower" test import "@source/lower"
test import "@source/interpreter" test import "@source/interpreter"
+327 -16
View File
@@ -100,6 +100,8 @@ State :: struct {
type_execution_depth usize = 0 type_execution_depth usize = 0
type_execution_remaining usize = 0 type_execution_remaining usize = 0
value_expansion_depth 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. #! 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 @hide:file
expand proc(state @mut State, id ast.NodeId) void ! Error { 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) try refresh(state)
if (state.expanded.items[usize(id)] != 0) return if (state.expanded.items[usize(id)] != 0) return
state.expanded.items[usize(id)] = 1 state.expanded.items[usize(id)] = 1
class :: ast.node_class(&state.tree, id) 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) { if (class == .expression and ast.expr_kind(&state.tree, id) == .block_expr) {
body :: ast.block_expr(&state.tree, id) 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)) { if (is_type(state, annotation) or type_record_storage(state, annotation, 0)) {
selected :: try evaluate_type_value_block(state, id, annotation) selected :: try evaluate_type_value_block(state, id, annotation)
if (selected == no_node) { if (selected == no_node) {
@@ -289,6 +344,7 @@ expand proc(state @mut State, id ast.NodeId) void ! Error {
} }
state.value_expansion_depth += 1 state.value_expansion_depth += 1
defer state.value_expansion_depth -= 1 defer state.value_expansion_depth -= 1
state.yield_type = annotation
try expand(state, body) try expand(state, body)
return 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) { if (class == .expression and ast.expr_kind(&state.tree, id) == .catch_expr) {
expression :: ast.catch_expr(&state.tree, id) expression :: ast.catch_expr(&state.tree, id)
try expand_expected(state, expression.operand, expected)
if (expression.capture) |capture| { if (expression.capture) |capture| {
try expand(state, expression.operand)
annotation :: try expression_type(state, expression.operand, 0) annotation :: try expression_type(state, expression.operand, 0)
if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .fallible_type) { 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| { 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) { if (class == .statement and ast.stmt_kind(&state.tree, id) == .expression) {
expression :: ast.statement_expr(&state.tree, id) expression :: ast.statement_expr(&state.tree, id)
if ( if (
ast.expr_kind(&state.tree, expression) == .match_expr 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 ) return
} }
if ( if (
class == .expression and ast.expr_kind(&state.tree, id) == .match_expr 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 ) return
if (class == .statement and ast.stmt_kind(&state.tree, id) == .if_stmt) { if (class == .statement and ast.stmt_kind(&state.tree, id) == .if_stmt) {
conditional :: ast.if_stmt(&state.tree, id) 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) { if (class == .expression and ast.expr_kind(&state.tree, id) == .comptime_expr) {
operand :: ast.comptime_operand(&state.tree, id) operand :: ast.comptime_operand(&state.tree, id)
try expand_expected(state, operand, expected)
state.evaluation_diagnostic = diagnostics.no_diagnostic state.evaluation_diagnostic = diagnostics.no_diagnostic
value :: try evaluate(state, operand) value :: try evaluate(state, operand)
if (value.annotation == no_node or !type_record_storage(state, value.annotation, 0)) 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) { if (class == .expression and ast.expr_kind(&state.tree, id) == .builtin_call) {
name :: ast.builtin_call(&state.tree, id).name 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")) { if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "compile_error")) {
try expand_compile_error(state, id) try expand_compile_error(state, id)
return 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 ast.node_class(&state.tree, resolved.data.declaration) == .function
and template(state, resolved.data.declaration) and template(state, resolved.data.declaration)
) { ) {
try specialize_call(state, id, resolved.data.declaration) try specialize_call(state, id, resolved.data.declaration, expected)
return 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) count :: ast.child_count(&state.tree, id)
for (0..count) |i| { 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 # runtime bodies share the type-producer selector, but retain ordinary runtime
# parameters and control transfers in the selected arm. # parameters and control transfers in the selected arm.
@hide:file @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 explicit :: ast.expr_kind(&state.tree, ast.match_expr(&state.tree, id).subject) == .comptime_expr
try expand_inline_match(state, id, no_node) try expand_inline_match(state, id, no_node)
if (ast.expr_kind(&state.tree, id) != .match_expr) return true 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) try reject_evaluation(state, id)
return true return true
} }
try expand(state, selected) try expand_expected(state, selected, expected)
if (statement != no_node) { if (statement != no_node) {
replacement :: try ast.add_expression_stmt(state.builder, selected) replacement :: try ast.add_expression_stmt(state.builder, selected)
try replace(state, statement, replacement) try replace(state, statement, replacement)
@@ -1632,7 +1808,48 @@ compiletime_result proc(state @State, annotation ast.NodeId) bool {
} }
@hide:file @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) { if (state.depth >= 128 or state.instances.items.len >= 4096) {
try reject(state, call, .specialization_limit) try reject(state, call, .specialization_limit)
return return
@@ -1664,8 +1881,9 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error {
if (view.comptime and !explicit) continue if (view.comptime and !explicit) continue
argument :: ast.call_arg(&state.tree, call, position) argument :: ast.call_arg(&state.tree, call, position)
position += 1 position += 1
try expand(state, argument)
if (type_result or view.comptime) { 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 state.evaluation_diagnostic = diagnostics.no_diagnostic
annotation := view.type annotation := view.type
contextual := contextual_literal(state, argument) contextual := contextual_literal(state, argument)
@@ -1689,7 +1907,16 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error {
} }
} }
if (!explicit) { 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 position = 0
for (0..count) |i| { for (0..count) |i| {
parameter :: ast.function_param(&state.tree, function, 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 if (view.comptime) continue
argument :: runtime_arguments.items[position] argument :: runtime_arguments.items[position]
position += 1 position += 1
contextual := flexible(state, argument) or contextual_literal(state, argument) contextual :: inference_contextual(state, argument)
if (ast.expr_kind(&state.tree, argument) == .record_literal) { if (inference_phase(state, argument) != phase) continue
annotation :: ast.record_literal(&state.tree, argument).type if (annotation_ready(state, function, view.type, arguments, 0)) {
contextual = annotation == null or ast.expr_kind(&state.tree, annotation?) == .enum_literal 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) actual :: try expression_type(state, argument, 0)
# contextual literals need not have an independent type; the rewritten # contextual literals need not have an independent type; the rewritten
# call checks them against the substituted signature after inference. # 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 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) key := usize(function)
for arguments |argument| { for arguments |argument| {
if (argument.kind != .invalid) key = mix(key, try value_hash(state, 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 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 @hide:file
argument_annotation proc( argument_annotation proc(
state @mut State, function, annotation ast.NodeId, arguments []Value, state @mut State, function, annotation ast.NodeId, arguments []Value,
) ast.NodeId ! Error { ) ast.NodeId ! Error {
if (!annotation_generic(state, function, annotation, 0)) return annotation
mapping :: try mem.alloc(ast.NodeId, state.allocator, state.original_count) mapping :: try mem.alloc(ast.NodeId, state.allocator, state.original_count)
defer mem.free(state.allocator, mapping) defer mem.free(state.allocator, mapping)
mem.set(mapping, no_node) mem.set(mapping, no_node)
@@ -2057,6 +2309,7 @@ argument_annotation proc(
@hide:file @hide:file
evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Error { 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 (!is_type(state, annotation) and type_record_storage(state, annotation, 0)) {
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1 state.type_execution_depth += 1
@@ -2667,6 +2920,7 @@ type_snapshot_catch proc(
view :: ast.catch_expr(&state.tree, id) view :: ast.catch_expr(&state.tree, id)
operand :: try type_snapshot(state, execution, view.operand) operand :: try type_snapshot(state, execution, view.operand)
if (operand == no_node) return no_node if (operand == no_node) return no_node
try expand_expected(state, operand, annotation)
operand_type :: try expression_type(state, operand, 0) 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 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) 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 return concrete.node
} }
if (!try expand_type_matches(state, expression)) return no_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) resolved :: binding(state, expression)
if (resolved.kind == .invalid) { if (resolved.kind == .invalid) {
state.evaluation_diagnostic = resolved.data.diagnostic 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 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) 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: { .call: {
callee :: binding(state, ast.call_expr(&state.tree, pattern).callee) callee :: binding(state, ast.call_expr(&state.tree, pattern).callee)
if (callee.kind != .declaration) return false 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 @hide:file
materialize proc(state @mut State, value Value) Value ! Error { materialize proc(state @mut State, value Value) Value ! Error {
result := value result := value
+127 -3
View File
@@ -11,7 +11,7 @@ import "@source/parser"
@hide:file @hide:file
expect_specialization proc( expect_specialization proc(
source []u8, expected diagnostics.Code, expected_count usize, 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) strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings) defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator) 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 } `observe proc($T type, values []T) usize { return values.len }
`reserve proc($T type, storage @mut Storage(T), count usize) void { `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 { `append proc($T type, storage @mut Storage(T), value T) void {
` reserve(storage, storage.items.len) ` 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 { `release proc($T type, storage @mut Storage(T)) void {
` allocation :: storage.items.ptr[..storage.capacity] ` allocation :: storage.items.ptr[..storage.capacity]
` _ = observe(allocation) ` observe(allocation)
`} `}
`forward proc(storage @mut Storage(i16)) void { `forward proc(storage @mut Storage(i16)) void {
` append(storage, 7) ` append(storage, 7)
@@ -127,3 +127,127 @@ infers_integer_builtin_results_from_their_type_or_rigid_operand test {
, .cannot_infer_generic, 0, , .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,
)
}
+157 -11
View File
@@ -998,6 +998,153 @@ CASES = {
} }
""", b"42\n", None, """, 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": ( "generic_contextual_enum_rejects_unknown_member": (
""" """
Key :: enum { first } Key :: enum { first }
@@ -9554,7 +9701,7 @@ def check_hashmap_packages(temporary, executable):
return left - divtrunc!(left, 100) * 100 == right - divtrunc!(right, 100) * 100 return left - divtrunc!(left, 100) * 100 == right - divtrunc!(right, 100) * 100
} }
exercise proc(allocator mem.Allocator, state @mut State) i16 ! (hashmap.PutError | mem.AllocError) { 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) defer hashmap.deinit(&map)
if (map.count != 0 or map.entries.len != 0 or state.allocations != 0) return -1 if (map.count != 0 or map.entries.len != 0 or state.allocations != 0) return -1
if (hashmap.get(&map, 0) != null) return -2 if (hashmap.get(&map, 0) != null) return -2
@@ -9604,7 +9751,7 @@ def check_hashmap_packages(temporary, executable):
return 42 return 42
} }
exercise_automatic_and_string_keys proc(allocator mem.Allocator) i16 ! (hashmap.PutError | mem.AllocError) { 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) defer hashmap.deinit(&automatic)
for (i16(0)..i16(12)) |key| { for (i16(0)..i16(12)) |key| {
try hashmap.put(&automatic, key, Value{ number = key, items = [key, key + 1] }) 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 if (hashmap.get(&automatic, minval!(i16))?.number != 42
or hashmap.get(&automatic, maxval!(i16))?.items[0] != 19) return -21 or hashmap.get(&automatic, maxval!(i16))?.items[0] != 19) return -21
if (hashmap.get(&automatic, -1) != null) return -22 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) defer hashmap.deinit(&strings)
left [3]mut u8 := [97, 0, 98] left [3]mut u8 := [97, 0, 98]
right [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 Inner :: distinct u64
Id :: distinct Inner Id :: distinct Inner
integer_keys proc($K type) bool ! (hashmap.PutError | mem.AllocError) { 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) defer hashmap.deinit(&map)
keys [4]K :: [minval!(K), maxval!(K), K(1), K(2)] keys [4]K :: [minval!(K), maxval!(K), K(1), K(2)]
for keys |key, index| { try hashmap.put(&map, key, index) } 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(i32) or !try integer_keys(u32)) return -3
if (!try integer_keys(i64) or !try integer_keys(u64)) return -4 if (!try integer_keys(i64) or !try integer_keys(u64)) return -4
if (!try integer_keys(isize) or !try integer_keys(usize)) return -5 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) defer hashmap.deinit(&flags)
try hashmap.put(&flags, false, 19) try hashmap.put(&flags, false, 19)
try hashmap.put(&flags, true, 23) try hashmap.put(&flags, true, 23)
@@ -9706,14 +9853,14 @@ def check_hashmap_packages(temporary, executable):
duplicate := false duplicate := false
hashmap.put(&flags, false, 99) catch |err| { duplicate = err == .key_exists } 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 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) defer hashmap.deinit(&sparse)
try hashmap.put(&sparse, .negative, 7) try hashmap.put(&sparse, .negative, 7)
try hashmap.put(&sparse, .first, 3) try hashmap.put(&sparse, .first, 3)
try hashmap.put(&sparse, .far, 90) try hashmap.put(&sparse, .far, 90)
if (sparse.count != 3 or hashmap.get(&sparse, Sparse.negative)? != 7) return -8 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 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) defer hashmap.deinit(&ids)
try hashmap.put(&ids, Id(Inner(0)), 19) try hashmap.put(&ids, Id(Inner(0)), 19)
try hashmap.put(&ids, Id(Inner(maxval!(u64))), 23) 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 return left - divtrunc!(left, 100) * 100 == right - divtrunc!(right, 100) * 100
} }
main proc() i16 ! (hashmap.PutError | mem.AllocError) { main proc() i16 ! (hashmap.PutError | mem.AllocError) {
# The alias supplies the library's private string callbacks. strings hashmap.StringHashMap(i16) := hashmap.init(mem.c_allocator)
strings hashmap.StringHashMap(i16) := { entries = &[], count = 0, allocator = mem.c_allocator }
defer hashmap.deinit(&strings) defer hashmap.deinit(&strings)
left [3]mut u8 := [97, 0, 98] left [3]mut u8 := [97, 0, 98]
right [3]mut u8 := [97, 0, 98] right [3]mut u8 := [97, 0, 98]
other [3]mut u8 := [97, 0, 99] other [3]mut u8 := [97, 0, 99]
if ((&left).ptr == (&right).ptr) return -7 if ((&left).ptr == (&right).ptr) return -7
try hashmap.put(&strings, left[..], 42) 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) defer hashmap.deinit(&records)
first Key := { padded = { tag = 7, number = maxval!(u64) }, rows = [{ values = [2, 9] }, { values = [4, 8] }] } 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] }] } 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, "", 7)
try hashmap.put(&strings, other[..], 23) try hashmap.put(&strings, other[..], 23)
if (strings.count != 3 or hashmap.get(&strings, "")? != 7 or hashmap.get(&strings, other[..])? != 23) return -11 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) defer hashmap.deinit(&custom)
for (u16(1)..u16(20)) |key| { try hashmap.put(&custom, key, i16(key * 2)) } for (u16(1)..u16(20)) |key| { try hashmap.put(&custom, key, i16(key * 2)) }
for (u16(1)..u16(20)) |key| { for (u16(1)..u16(20)) |key| {