From bbf39c16c5ab353be28950925d07e8f80c695f6e Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Thu, 8 Oct 2026 23:06:38 +0200 Subject: [PATCH] feat: type-check expressions and function calls --- README.md | 12 +- source/diagnostics/diagnostics.hon | 10 + source/main.hon | 7 +- source/sema/check/check.hon | 650 +++++++++++++++++++++++++++++ source/sema/check/check.test.hon | 608 +++++++++++++++++++++++++++ 5 files changed, 1283 insertions(+), 4 deletions(-) create mode 100644 source/sema/check/check.hon create mode 100644 source/sema/check/check.test.hon diff --git a/README.md b/README.md index cccfbb4..efc8b0e 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,16 @@ build Honey from this directory: bro build ``` -the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. +the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. parsing accepts integer division syntax, but type checking requires float operands for `/`. -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 a trailing comma; newlines remain statement boundaries. argument/type checking belongs to semantic analysis and is not yet implemented. +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 a trailing comma; newlines remain statement boundaries. type checking validates callable values, argument counts and argument types. name resolution (`@source/sema`) runs after parsing. it predeclares package names and function-body locals, allowing forward references and mutually recursive functions. parameters and locals share a function scope; signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `string`, `void`); sibling function scopes may independently reuse names. -`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax retains its existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration. type checking and read-before-initialization checks remain separate, unimplemented stages. +`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax retains its existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration. + +type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types; query with `check.type_of` and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations must name builtin types; type names are not ordinary values. `void` is valid as a function result, not as a parameter or stored value. + +numeric typing follows Bro's contextual rules within Honey's current `int` and `float` families. integer literals can adopt either family; float literals remain float. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int` values do not implicitly convert to `float`. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` is float-only. `3 / 2` is invalid without float context, while `3.0 / 2` and `ratio float :: 3 / 2` are valid. + +type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions and arguments are still checked. inference cycles without a type anchor are diagnosed. read-before-initialization and runtime initialization-cycle checks remain separate, unimplemented work. function signatures are checked independently of their bodies; since return statements are not yet parsed, nonvoid functions currently report missing-return diagnostics. lowering and executable generation remain unimplemented. diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 7b80039..82c104d 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -16,6 +16,16 @@ Code :: enum { unexpected_token, duplicate_name, unknown_name, + expected_type, + type_not_value, + invalid_void_type, + type_mismatch, + invalid_operand, + integer_division, + not_callable, + argument_count_mismatch, + cannot_infer_type, + missing_return, } Diagnostic :: struct { diff --git a/source/main.hon b/source/main.hon index 7c632e6..fad8175 100644 --- a/source/main.hon +++ b/source/main.hon @@ -9,6 +9,7 @@ import "@source/parser" import "@source/sema" ast_renderer :: import "@source/ast/renderer" +typecheck :: import "@source/sema/check" test import "@std/mem" test import "@std/enummap" @@ -23,6 +24,7 @@ test import "@source/ast" test import "@source/ast/renderer" test import "@source/parser" test import "@source/sema" +test import "@source/sema/check" @hide:file program :: @@ -32,7 +34,7 @@ program :: `z :: "hello" ` `a := 1 + 2 * 3 - `b :: -3 / 2 + 1 + `b :: -3.0 / 2 + 1 ` `grouped proc(left, right int, extra float) void { ` value int :: later + left @@ -64,6 +66,9 @@ main proc() void! { resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) defer sema.deinit(&resolution) + checked := try typecheck.check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer typecheck.deinit(&checked) + renderer := ast_renderer.init(mem.c_allocator) defer ast_renderer.deinit(&renderer) try ast_renderer.render(&renderer, &tree) diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon new file mode 100644 index 0000000..a560ada --- /dev/null +++ b/source/sema/check/check.hon @@ -0,0 +1,650 @@ +import "@std/debug" +import "@std/mem" +import "@std/arraylist" +import "@source/ast" +import "@source/diagnostics" +sema :: import "@source/sema" + +TypeKind :: enum { none, invalid, int, float, string, void, function } + +Type :: struct { + kind TypeKind = .none + function ast.NodeId = ast.NodeId(maxval!(u32)) + diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic +} + +@hide:file +Storage :: struct { + types []mut Type + allocator mem.Allocator +} + +#! owns node-indexed types and retains its allocator; release with deinit. +#! function IDs refer to the caller-owned AST, not checker-owned signatures. +Result :: struct { storage Storage } + +@hide:file +no_index :: maxval!(usize) + +@hide:file +Role :: enum { none, expression, declaration, parameter, function, annotation, invalid, body } + +@hide:file +Cell :: struct { + role Role = .none + parent usize = 0 + size usize = 1 + type Type = Type{} + numeric bool = false + anchored bool = false + demand TypeKind = .none + fixed bool = false + first_edge usize = no_index +} + +@hide:file +Edge :: struct { + dependent ast.NodeId + next usize +} + +@hide:file +State :: struct { + tree @ast.Tree + resolution @sema.Resolution + diagnostics @mut diagnostics.Store + types []mut Type + cells []mut Cell + edges arraylist.ArrayList(Edge) +} + +#! borrows the AST, resolution, and diagnostic store only during checking. +#! temporary constraints are freed on every exit; Result owns only its type table. +check proc( + allocator mem.Allocator, + tree @ast.Tree, + resolution @sema.Resolution, + diagnostic_store @mut diagnostics.Store, +) Result ! (mem.AllocError | diagnostics.CapacityError) { + count :: ast.node_count(tree) + types :: try mem.alloc(Type, allocator, count) + errdefer mem.free(allocator, types) + mem.set(types, Type{}) + cells :: try mem.alloc(Cell, allocator, count) + defer mem.free(allocator, cells) + mem.set(cells, Cell{}) + for (0..count) |i| cells[i].parent = i + queue :: try mem.alloc(ast.NodeId, allocator, count) + defer mem.free(allocator, queue) + state := State{ + tree = tree, + resolution = resolution, + diagnostics = diagnostic_store, + types = types, + cells = cells, + edges = arraylist.init(allocator), + } + defer arraylist.deinit(&state.edges) + + for (0..ast.package_decl_count(tree)) |i| { + id :: ast.package_decl(tree, i) + match ast.decl_kind(tree, id) { + .statement: try collect_declaration(&state, id) + .function: try collect_function(&state, id) + .invalid: collect_invalid(&state, id) + } + } + + # aliases share a type variable; explicit annotations and parameters are boundaries. + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue + binding :: sema.binding(resolution, id) + if (binding.kind != .declaration) continue + target :: binding.data.declaration + try depend(&state, target, id) + if (cells[usize(target)].fixed) cells[i].type = cells[usize(target)].type + } + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role == .declaration and !cells[i].fixed) { + join(&state, id, ast.statement_decl(tree, id).initializer) + } + if (cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue + binding :: sema.binding(resolution, id) + if (binding.kind == .declaration and !cells[usize(binding.data.declaration)].fixed) { + join(&state, id, binding.data.declaration) + } + } + + # function aliases are known before any calls or bodies are checked. Honey's + # return annotations are scalar, so a call cannot introduce a function type. + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue + callee :: ast.call_expr(tree, id).callee + signature :: group_type(&state, callee) + if (signature.function != ast.NodeId(maxval!(u32))) { + result :: ast.function_decl(tree, signature.function).return_type + seed(&state, id, types[usize(result)]) + } else if (signature.kind == .invalid) { + seed(&state, id, signature) + } + } + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue + callee :: ast.call_expr(tree, id).callee + signature :: group_type(&state, callee) + if (signature.function != ast.NodeId(maxval!(u32))) continue + if (signature.kind == .invalid) { + seed(&state, id, signature) + } else if (signature.kind != .none or cells[root(&state, usize(callee))].numeric) { + seed(&state, id, invalid_type(try diagnostics.add(diagnostic_store, .not_callable, ast.node_span(tree, callee)))) + } else { + # without a function anchor this dependency cannot become callable; + # share its eventual inference error instead of emitting a cascade. + join(&state, id, callee) + } + } + for (0..count) |i| { + if (cells[i].role == .none or cells[i].role == .body) continue + types[i] = cells[root(&state, i)].type + } + # reject inherently nonnumeric operations before connecting their operands. + # their otherwise valid inputs must not join a failed constraint component. + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression) continue + match ast.expr_kind(tree, id) { + .unary: try require_numeric(&state, id, ast.unary_expr(tree, id).operand) + .binary: { + expression :: ast.binary_expr(tree, id) + try require_numeric(&state, id, expression.left) + try require_numeric(&state, id, expression.right) + } + else: {} + } + } + propagate(&state, queue) + + # numeric edges are undirected constraints, not a traversal-time conversion. + # rigid values supply demands without joining the component they constrain. + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression) continue + match ast.expr_kind(tree, id) { + .unary: try numeric_operand(&state, id, ast.unary_expr(tree, id).operand) + .binary: { + expression :: ast.binary_expr(tree, id) + try numeric_operand(&state, id, expression.left) + try numeric_operand(&state, id, expression.right) + } + else: {} + } + } + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role == .declaration) { + declaration :: ast.statement_decl(tree, id) + if (declaration.type) |annotation| { + try demand(&state, declaration.initializer, types[usize(annotation)], .type_mismatch) + } + } + if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue + signature :: group_type(&state, ast.call_expr(tree, id).callee) + if (signature.function == ast.NodeId(maxval!(u32))) continue + parameter_count :: ast.function_param_count(tree, signature.function) + for (0..ast.call_arg_count(tree, id)) |j| { + if (j >= parameter_count) continue + parameter :: ast.function_param(tree, signature.function, j) + try demand(&state, ast.call_arg(tree, id, j), cells[usize(parameter)].type, .type_mismatch) + } + } + + # default only after the complete program has supplied its numeric demands. + for (0..count) |i| { + if (root(&state, i) != i) continue + if (cells[i].type.kind == .invalid) continue + if (cells[i].numeric and cells[i].anchored) { + kind TypeKind :: if (cells[i].demand == .float) .float else .int + cells[i].type = Type{ kind = kind } + } + } + for (0..count) |i| { + id :: ast.NodeId(i) + if (cells[i].role != .expression or ast.expr_kind(tree, id) != .call) continue + callee :: ast.call_expr(tree, id).callee + signature :: group_type(&state, callee) + if (signature.function != ast.NodeId(maxval!(u32)) or signature.kind == .none or signature.kind == .invalid) continue + if (group_type(&state, id).kind != .invalid) { + seed(&state, id, invalid_type(try diagnostics.add(diagnostic_store, .not_callable, ast.node_span(tree, callee)))) + } + } + for (0..count) |i| { + if (cells[i].role != .declaration or types[i].kind == .invalid) continue + index :: root(&state, i) + if (cells[index].type.kind != .none) continue + diagnostic :: try diagnostics.add(diagnostic_store, .cannot_infer_type, ast.statement_decl(tree, ast.NodeId(i)).name) + cells[index].type = invalid_type(diagnostic) + } + for (0..count) |i| { + if (cells[i].role == .none or cells[i].role == .body or types[i].kind == .invalid) continue + types[i] = cells[root(&state, i)].type + } + for (0..count) |i| try validate(&state, ast.NodeId(i)) + + # errors travel only from dependencies to users. A rejected use never poisons + # an explicitly typed declaration, and body errors do not erase signatures. + propagate(&state, queue) + return Result{ storage = Storage{ types = types, allocator = allocator } } +} + +#! returns a type without retaining a reference to checker temporaries. +type_of proc(result @Result, id ast.NodeId) Type { + debug.assert(usize(id) < result.storage.types.len) + return result.storage.types[usize(id)] +} + +#! releases type storage using the allocator retained by check. +deinit proc(result @mut Result) void { + mem.free(result.storage.allocator, result.storage.types) +} + +@hide:file +propagate proc(state @mut State, queue []mut ast.NodeId) void { + next := usize(0) + end := usize(0) + for (0..(state.types.len)) |i| { + if (state.types[i].kind != .invalid) continue + queue[end] = ast.NodeId(i) + end += 1 + } + while (next < end) { + source :: queue[next] + next += 1 + edge := state.cells[usize(source)].first_edge + while (edge != no_index) { + dependent :: state.edges.items[edge].dependent + if (state.types[usize(dependent)].kind != .invalid) { + state.types[usize(dependent)] = invalid_type(state.types[usize(source)].diagnostic) + queue[end] = dependent + end += 1 + } + edge = state.edges.items[edge].next + } + } +} + +@hide:file +require_numeric proc(state @mut State, expression, operand ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + index :: root(state, usize(operand)) + type :: state.cells[index].type + if (type.kind == .invalid) { + state.types[usize(expression)] = type + return + } + if (type.kind == .none or state.cells[index].numeric or numeric_kind(type.kind)) return + try fail(state, expression, .invalid_operand, ast.node_span(state.tree, expression)) +} + +@hide:file +invalid_type proc(diagnostic diagnostics.DiagnosticId) Type { + return Type{ kind = .invalid, diagnostic = diagnostic } +} + +@hide:file +root proc(state @mut State, index usize) usize { + current := index + while (state.cells[current].parent != current) current = state.cells[current].parent + next := index + while (state.cells[next].parent != next) { + parent :: state.cells[next].parent + state.cells[next].parent = current + next = parent + } + return current +} + +@hide:file +group_type proc(state @mut State, id ast.NodeId) Type { + return state.cells[root(state, usize(id))].type +} + +@hide:file +seed proc(state @mut State, id ast.NodeId, type Type) void { + index :: root(state, usize(id)) + if (state.cells[index].type.kind != .invalid) state.cells[index].type = type +} + +@hide:file +join proc(state @mut State, left, right ast.NodeId) void { + a := root(state, usize(left)) + b := root(state, usize(right)) + if (a == b) return + if (state.cells[a].size < state.cells[b].size) { + saved :: a + a = b + b = saved + } + state.cells[b].parent = a + state.cells[a].size += state.cells[b].size + if (state.cells[a].type.kind == .none or state.cells[b].type.kind == .invalid) { + state.cells[a].type = state.cells[b].type + } + state.cells[a].numeric = state.cells[a].numeric or state.cells[b].numeric + state.cells[a].anchored = state.cells[a].anchored or state.cells[b].anchored + if (state.cells[a].demand == .none) state.cells[a].demand = state.cells[b].demand +} + +@hide:file +depend proc(state @mut State, source, dependent ast.NodeId) void ! mem.AllocError { + next :: state.cells[usize(source)].first_edge + try arraylist.append(&state.edges, Edge{ dependent = dependent, next = next }) + state.cells[usize(source)].first_edge = state.edges.items.len - 1 +} + +@hide:file +collect_invalid proc(state @mut State, id ast.NodeId) void { + state.cells[usize(id)].role = .invalid + state.cells[usize(id)].type = invalid_type(ast.invalid(state.tree, id).diagnostic) +} + +@hide:file +check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (mem.AllocError | diagnostics.CapacityError) { + if (state.cells[usize(id)].role == .annotation) return state.types[usize(id)] + state.cells[usize(id)].role = .annotation + binding :: sema.binding(state.resolution, id) + type := Type{} + match binding.kind { + .invalid: type = invalid_type(binding.data.diagnostic) + .builtin: { + type.kind = match binding.data.builtin { + .int: .int + .float: .float + .string: .string + .void: .void + } + if (type.kind == .void and !allow_void) { + type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_void_type, ast.node_span(state.tree, id))) + } + } + else: type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id))) + } + state.types[usize(id)] = type + state.cells[usize(id)].type = type + return type +} + +@hide:file +collect_declaration proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + declaration :: ast.statement_decl(state.tree, id) + state.cells[usize(id)].role = .declaration + if (declaration.type) |type| { + state.cells[usize(id)].fixed = true + state.cells[usize(id)].type = try check_annotation(state, type, false) + try depend(state, type, id) + } + binding :: sema.binding(state.resolution, id) + if (binding.kind == .invalid) { + state.cells[usize(id)].fixed = true + state.cells[usize(id)].type = invalid_type(binding.data.diagnostic) + } + try collect_expression(state, declaration.initializer) + try depend(state, declaration.initializer, id) +} + +@hide:file +collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + tree :: state.tree + function :: ast.function_decl(tree, id) + state.cells[usize(id)].role = .function + state.cells[usize(id)].fixed = true + state.cells[usize(id)].type = Type{ kind = .function, function = id } + _ = try check_annotation(state, function.return_type, true) + try depend(state, function.return_type, id) + for (0..ast.function_param_count(tree, id)) |i| { + parameter :: ast.function_param(tree, id, i) + state.cells[usize(parameter)].role = .parameter + state.cells[usize(parameter)].fixed = true + state.cells[usize(parameter)].type = try check_annotation(state, ast.param(tree, parameter).type, false) + binding :: sema.binding(state.resolution, parameter) + if (binding.kind == .invalid) state.cells[usize(parameter)].type = invalid_type(binding.data.diagnostic) + try depend(state, ast.param(tree, parameter).type, parameter) + try depend(state, parameter, id) + } + binding :: sema.binding(state.resolution, id) + if (binding.kind == .invalid) { + state.cells[usize(id)].type = invalid_type(binding.data.diagnostic) + state.cells[usize(id)].type.function = id + } + state.cells[usize(function.body)].role = .body + for (0..ast.block_statement_count(tree, function.body)) |i| { + statement :: ast.block_statement(tree, function.body, i) + match ast.stmt_kind(tree, statement) { + .declaration: try collect_declaration(state, statement) + .expression: try collect_expression(state, ast.statement_expr(tree, statement)) + .invalid: collect_invalid(state, statement) + } + source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else statement + try depend(state, source, function.body) + } +} + +@hide:file +collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + if (state.cells[usize(id)].role != .none) return + tree :: state.tree + state.cells[usize(id)].role = .expression + match ast.expr_kind(tree, id) { + .literal_int: { + state.cells[usize(id)].numeric = true + state.cells[usize(id)].anchored = true + } + .literal_float: state.cells[usize(id)].type = Type{ kind = .float } + .literal_string: state.cells[usize(id)].type = Type{ kind = .string } + .identifier: { + binding :: sema.binding(state.resolution, id) + match binding.kind { + .invalid: state.cells[usize(id)].type = invalid_type(binding.data.diagnostic) + .builtin: state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id))) + else: {} + } + } + .unary: { + state.cells[usize(id)].numeric = true + operand :: ast.unary_expr(tree, id).operand + try collect_expression(state, operand) + try depend(state, operand, id) + } + .binary: { + state.cells[usize(id)].numeric = true + expression :: ast.binary_expr(tree, id) + try collect_expression(state, expression.left) + try collect_expression(state, expression.right) + try depend(state, expression.left, id) + try depend(state, expression.right, id) + } + .call: { + callee :: ast.call_expr(tree, id).callee + try collect_expression(state, callee) + try depend(state, callee, id) + for (0..ast.call_arg_count(tree, id)) |i| { + argument :: ast.call_arg(tree, id, i) + try collect_expression(state, argument) + try depend(state, argument, id) + } + } + .invalid: collect_invalid(state, id) + } +} + +@hide:file +numeric_kind proc(kind TypeKind) bool { + return kind == .int or kind == .float +} + +@hide:file +impose proc(state @mut State, id ast.NodeId, kind TypeKind, code diagnostics.Code, span ast.Span) void ! (mem.AllocError | diagnostics.CapacityError) { + index :: root(state, usize(id)) + if (state.cells[index].type.kind == .invalid) return + state.cells[index].anchored = true + if (state.cells[index].demand == .none) { + state.cells[index].demand = kind + } else if (state.cells[index].demand != kind) { + state.cells[index].type = invalid_type(try diagnostics.add(state.diagnostics, code, span)) + } +} + +@hide:file +numeric_operand proc(state @mut State, expression, operand ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + index :: root(state, usize(operand)) + type :: state.cells[index].type + if (state.types[usize(expression)].kind == .invalid or state.types[usize(operand)].kind == .invalid) return + if (group_type(state, expression).kind == .invalid) return + if (state.cells[index].numeric or type.kind == .none) { + expression_demand :: state.cells[root(state, usize(expression))].demand + operand_demand :: state.cells[index].demand + join(state, expression, operand) + if (expression_demand != .none) { + try impose(state, expression, expression_demand, .invalid_operand, ast.node_span(state.tree, expression)) + } + if (operand_demand != .none) { + try impose(state, expression, operand_demand, .invalid_operand, ast.node_span(state.tree, expression)) + } + } else if (numeric_kind(type.kind)) { + try impose(state, expression, type.kind, .invalid_operand, ast.node_span(state.tree, expression)) + } +} + +@hide:file +demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code) void ! (mem.AllocError | diagnostics.CapacityError) { + if (!numeric_kind(expected.kind) or state.types[usize(id)].kind == .invalid) return + index :: root(state, usize(id)) + if (!state.cells[index].numeric and state.cells[index].type.kind != .none) return + state.cells[index].numeric = true + try impose(state, id, expected.kind, code, ast.node_span(state.tree, id)) +} + +@hide:file +fail proc(state @mut State, id ast.NodeId, code diagnostics.Code, span ast.Span) void ! (mem.AllocError | diagnostics.CapacityError) { + if (state.types[usize(id)].kind == .invalid) return + state.types[usize(id)] = invalid_type(try diagnostics.add(state.diagnostics, code, span)) +} + +@hide:file +compatible proc(state @mut State, value ast.NodeId, expected Type) void ! (mem.AllocError | diagnostics.CapacityError) { + actual :: state.types[usize(value)] + if (actual.kind == .invalid or expected.kind == .invalid) return + if (actual.kind != expected.kind) try fail(state, value, .type_mismatch, ast.node_span(state.tree, value)) +} + +@hide:file +validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + tree :: state.tree + match state.cells[usize(id)].role { + .declaration: { + declaration :: ast.statement_decl(tree, id) + if (state.types[usize(declaration.initializer)].kind == .void) { + try fail(state, declaration.initializer, .type_mismatch, ast.node_span(tree, declaration.initializer)) + } + if (declaration.type) |type| try compatible(state, declaration.initializer, state.types[usize(type)]) + } + .function: { + function :: ast.function_decl(tree, id) + result :: state.types[usize(function.return_type)] + if (result.kind == .void or result.kind == .invalid or state.types[usize(id)].kind == .invalid) return + if (syntactically_valid_body(tree, function.body)) { + diagnostic :: try diagnostics.add(state.diagnostics, .missing_return, ast.node_span(tree, function.body)) + if (state.types[usize(function.body)].kind != .invalid) { + state.types[usize(function.body)] = invalid_type(diagnostic) + } + } + } + .expression: { + match ast.expr_kind(tree, id) { + .unary: { + operand :: state.types[usize(ast.unary_expr(tree, id).operand)] + if (operand.kind != .invalid and !numeric_kind(operand.kind)) { + try fail(state, id, .invalid_operand, ast.node_span(tree, id)) + } + } + .binary: { + expression :: ast.binary_expr(tree, id) + left :: state.types[usize(expression.left)] + right :: state.types[usize(expression.right)] + if (left.kind == .invalid or right.kind == .invalid) return + if (!numeric_kind(left.kind) or !numeric_kind(right.kind) or left.kind != right.kind) { + try fail(state, id, .invalid_operand, expression.span) + } else if (expression.operator == .divide and left.kind == .int) { + try fail(state, id, .integer_division, expression.span) + } + } + .call: try validate_call(state, id) + else: {} + } + } + else: {} + } +} + +@hide:file +validate_call proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + tree :: state.tree + expression :: ast.call_expr(tree, id) + callee :: state.types[usize(expression.callee)] + signature :: group_type(state, expression.callee) + if (signature.function == ast.NodeId(maxval!(u32))) { + if (callee.kind != .invalid) { + try fail(state, id, .not_callable, ast.node_span(tree, expression.callee)) + } + return + } + parameter_count :: ast.function_param_count(tree, signature.function) + if (parameter_count != ast.call_arg_count(tree, id)) { + diagnostic :: try diagnostics.add(state.diagnostics, .argument_count_mismatch, expression.span) + if (callee.kind != .invalid) state.types[usize(id)] = invalid_type(diagnostic) + } + for (0..ast.call_arg_count(tree, id)) |i| { + if (i >= parameter_count) continue + parameter :: ast.function_param(tree, signature.function, i) + try compatible(state, ast.call_arg(tree, id, i), state.types[usize(parameter)]) + } +} + +@hide:file +syntactically_valid_expression proc(tree @ast.Tree, id ast.NodeId) bool { + match ast.expr_kind(tree, id) { + .invalid: return false + .unary: return syntactically_valid_expression(tree, ast.unary_expr(tree, id).operand) + .binary: { + expression :: ast.binary_expr(tree, id) + return syntactically_valid_expression(tree, expression.left) and syntactically_valid_expression(tree, expression.right) + } + .call: { + if (!syntactically_valid_expression(tree, ast.call_expr(tree, id).callee)) return false + for (0..ast.call_arg_count(tree, id)) |i| { + if (!syntactically_valid_expression(tree, ast.call_arg(tree, id, i))) return false + } + } + else: {} + } + return true +} + +@hide:file +syntactically_valid_body proc(tree @ast.Tree, id ast.NodeId) bool { + for (0..ast.block_statement_count(tree, id)) |i| { + statement :: ast.block_statement(tree, id, i) + match ast.stmt_kind(tree, statement) { + .invalid: return false + .expression: { + if (!syntactically_valid_expression(tree, ast.statement_expr(tree, statement))) return false + } + .declaration: { + if (!syntactically_valid_expression(tree, ast.statement_decl(tree, statement).initializer)) return false + } + } + } + return true +} diff --git a/source/sema/check/check.test.hon b/source/sema/check/check.test.hon new file mode 100644 index 0000000..2ecbf6c --- /dev/null +++ b/source/sema/check/check.test.hon @@ -0,0 +1,608 @@ +import "@std/mem" +import "@std/testing" + +import "@source/diagnostics" +import "@source/strpool" +import "@source/lexer" +import "@source/parser" +import "@source/ast" +sema :: import "@source/sema" + +@hide:file +expect_kind proc(result @Result, id ast.NodeId, expected TypeKind) void ! testing.Error { + try testing.expect_equal(expected, type_of(result, id).kind) + try testing.expect_equal(diagnostics.no_diagnostic, type_of(result, id).diagnostic) +} + +@hide:file +expect_error proc( + result @Result, + diagnostic_store @diagnostics.Store, + id ast.NodeId, + code diagnostics.Code, + span ast.Span, +) diagnostics.DiagnosticId ! testing.Error { + actual :: type_of(result, id) + try testing.expect_equal(TypeKind.invalid, actual.kind) + if (diagnostics.get(diagnostic_store, actual.diagnostic)) |diagnostic| { + try testing.expect_equal(code, diagnostic.code) + try testing.expect_equal(span.start, diagnostic.span.start) + try testing.expect_equal(span.end, diagnostic.span.end) + } else { + return .expectation_failed + } + return actual.diagnostic +} + +@hide:file +code_count proc(diagnostic_store @diagnostics.Store, code diagnostics.Code) usize { + count := usize(0) + for 0..diagnostics.count(diagnostic_store) |index| { + if (diagnostics.get(diagnostic_store, diagnostics.DiagnosticId(index))) |diagnostic| { + if (diagnostic.code == code) count += 1 + } + } + return count +} + +propagates_numeric_context_before_defaulting_forward_and_mutable_aliases test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `forward float :: -alias * 2 + seed + `alias :: seed + `seed := 3 + `backward :: 4 + `demand float :: backward + `quotient float :: 3 / 2 + `mixed :: 3.0 / 2 + `plain :: -3 + 2 * 4 + `fraction :: 1.5 + `text :: "honey" + `fixed int :: 7 + `anchored :: fixed + 2 + `nested float :: quotient / (alias + 1) + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + expected [13]TypeKind :: [ + .float, .float, .float, .float, .float, .float, .float, + .int, .float, .string, .int, .int, .float, + ] + for expected |kind, index| { + declaration :: ast.package_decl(&tree, index) + view :: ast.statement_decl(&tree, declaration) + try expect_kind(&result, declaration, kind) + try expect_kind(&result, view.initializer, kind) + if (view.type) |annotation| try expect_kind(&result, annotation, kind) + } + forward :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer) + multiply :: ast.binary_expr(&tree, forward.left) + negate :: ast.unary_expr(&tree, multiply.left) + numeric_nodes [6]ast.NodeId :: [ + forward.left, forward.right, multiply.left, multiply.right, negate.operand, + ast.statement_decl(&tree, ast.package_decl(&tree, 2)).initializer, + ] + for (numeric_nodes) |id| try expect_kind(&result, id, .float) + quotient :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 5)).initializer) + mixed :: ast.binary_expr(&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 6)).initializer) + division_operands [4]ast.NodeId :: [quotient.left, quotient.right, mixed.left, mixed.right] + for (division_operands) |id| try expect_kind(&result, id, .float) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) +} + +rejects_fixed_family_conversion_integer_division_and_conflicting_demands test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `fixed int :: 1 + `real float :: 2 + `widen float :: fixed + `narrow int :: 1.5 + `cross :: fixed + real + `division :: 3 / 2 + `negated :: -"x" + `strings :: "a" + "b" + `first_int int :: conflict_a + `first_float float :: conflict_a + `conflict_a := 5 + `reverse_float float :: conflict_b + `reverse_int int :: conflict_b + `conflict_b :: 6 + `good :: 9 + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + try expect_kind(&result, ast.package_decl(&tree, 0), .int) + try expect_kind(&result, ast.package_decl(&tree, 1), .float) + codes [6]diagnostics.Code :: [ + .type_mismatch, .type_mismatch, .invalid_operand, + .integer_division, .invalid_operand, .invalid_operand, + ] + for codes |code, index| { + declaration :: ast.package_decl(&tree, index + 2) + expression :: ast.statement_decl(&tree, declaration).initializer + diagnostic :: try expect_error(&result, &diagnostic_store, expression, code, ast.node_span(&tree, expression)) + try testing.expect_equal(TypeKind.invalid, type_of(&result, declaration).kind) + try testing.expect_equal(diagnostic, type_of(&result, declaration).diagnostic) + } + conflicts [2]ast.NodeId :: [ast.package_decl(&tree, 10), ast.package_decl(&tree, 13)] + for conflicts |declaration| { + try testing.expect_equal(TypeKind.invalid, type_of(&result, declaration).kind) + diagnostic :: type_of(&result, declaration).diagnostic + if (diagnostics.get(&diagnostic_store, diagnostic)) |error| { + try testing.expect_equal(diagnostics.Code.type_mismatch, error.code) + } else { + return .expectation_failed + } + try testing.expect_equal(diagnostic, type_of(&result, ast.statement_decl(&tree, declaration).initializer).diagnostic) + } + conflict_uses [4]usize :: [8, 9, 11, 12] + for conflict_uses |index| { + declaration :: ast.package_decl(&tree, index) + target :: if (index < 11) conflicts[0] else conflicts[1] + try testing.expect_equal(TypeKind.invalid, type_of(&result, declaration).kind) + try testing.expect_equal(type_of(&result, target).diagnostic, type_of(&result, declaration).diagnostic) + } + try expect_kind(&result, ast.package_decl(&tree, 14), .int) + try testing.expect_equal(0, code_count(&diagnostic_store, .cannot_infer_type)) +} + +checks_forward_alias_calls_grouped_parameters_and_mutual_recursion test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `alias :: alpha + `alpha proc(a, b int) void { + ` (alias)(a, b) + ` beta(a + b, seed) + `} + `beta proc(total int, amount float) void { + ` alpha(total, 2) + `} + `seed := 3 + `main proc() void { + ` beta(1, 2) + ` (alias)(3, 4) + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + alias_decl :: ast.package_decl(&tree, 0) + alpha :: ast.package_decl(&tree, 1) + beta :: ast.package_decl(&tree, 2) + seed :: ast.package_decl(&tree, 3) + main_function :: ast.package_decl(&tree, 4) + functions [3]ast.NodeId :: [alpha, beta, main_function] + for functions |function| { + try expect_kind(&result, function, .function) + try testing.expect_equal(function, type_of(&result, function).function) + try expect_kind(&result, ast.function_decl(&tree, function).return_type, .void) + } + try expect_kind(&result, alias_decl, .function) + try testing.expect_equal(alpha, type_of(&result, alias_decl).function) + try testing.expect_equal(alpha, type_of(&result, ast.statement_decl(&tree, alias_decl).initializer).function) + alpha_body :: ast.function_decl(&tree, alpha).body + beta_body :: ast.function_decl(&tree, beta).body + main_body :: ast.function_decl(&tree, main_function).body + calls [5]ast.NodeId :: [ + ast.statement_expr(&tree, ast.block_statement(&tree, alpha_body, 0)), + ast.statement_expr(&tree, ast.block_statement(&tree, alpha_body, 1)), + ast.statement_expr(&tree, ast.block_statement(&tree, beta_body, 0)), + ast.statement_expr(&tree, ast.block_statement(&tree, main_body, 0)), + ast.statement_expr(&tree, ast.block_statement(&tree, main_body, 1)), + ] + callees [5]ast.NodeId :: [alpha, beta, alpha, beta, alpha] + for calls |call, index| { + try expect_kind(&result, call, .void) + callee :: ast.call_expr(&tree, call).callee + try expect_kind(&result, callee, .function) + try testing.expect_equal(callees[index], type_of(&result, callee).function) + try expect_kind(&result, ast.call_arg(&tree, call, 0), .int) + expected TypeKind :: if (index == 1 or index == 3) .float else .int + try expect_kind(&result, ast.call_arg(&tree, call, 1), expected) + } + try expect_kind(&result, seed, .float) + try expect_kind(&result, ast.statement_decl(&tree, seed).initializer, .float) + a :: ast.function_param(&tree, alpha, 0) + b :: ast.function_param(&tree, alpha, 1) + try testing.expect_equal(ast.param(&tree, a).type, ast.param(&tree, b).type) + try expect_kind(&result, a, .int) + try expect_kind(&result, b, .int) + try expect_kind(&result, ast.param(&tree, a).type, .int) + try expect_kind(&result, ast.function_param(&tree, beta, 1), .float) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) +} + +separates_type_annotations_from_values_and_restricts_void_storage test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `value :: 1 + `bad value :: -"x" + `builtin :: int + `grouped proc(a, b value) value {} + `void_params proc(a, b void) void {} + `storage void :: 1 + `noop proc() void {} + `sink proc(p int) void {} + `holder :: noop() + `operand :: noop() + 1 + `main proc() void { + ` noop() + ` sink(noop()) + `} + `good string :: "ok" + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + bad :: ast.statement_decl(&tree, ast.package_decl(&tree, 1)) + if (bad.type) |annotation| { + _ = try expect_error(&result, &diagnostic_store, annotation, .expected_type, ast.node_span(&tree, annotation)) + } else { + return .expectation_failed + } + _ = try expect_error(&result, &diagnostic_store, bad.initializer, .invalid_operand, ast.node_span(&tree, bad.initializer)) + builtin :: ast.statement_decl(&tree, ast.package_decl(&tree, 2)).initializer + _ = try expect_error(&result, &diagnostic_store, builtin, .type_not_value, ast.node_span(&tree, builtin)) + grouped :: ast.package_decl(&tree, 3) + a :: ast.function_param(&tree, grouped, 0) + b :: ast.function_param(&tree, grouped, 1) + shared_type :: ast.param(&tree, a).type + try testing.expect_equal(shared_type, ast.param(&tree, b).type) + shared_error :: try expect_error(&result, &diagnostic_store, shared_type, .expected_type, ast.node_span(&tree, shared_type)) + try testing.expect_equal(shared_error, type_of(&result, a).diagnostic) + try testing.expect_equal(shared_error, type_of(&result, b).diagnostic) + return_type :: ast.function_decl(&tree, grouped).return_type + _ = try expect_error(&result, &diagnostic_store, return_type, .expected_type, ast.node_span(&tree, return_type)) + void_function :: ast.package_decl(&tree, 4) + void_a :: ast.function_param(&tree, void_function, 0) + void_b :: ast.function_param(&tree, void_function, 1) + void_annotation :: ast.param(&tree, void_a).type + void_error :: try expect_error(&result, &diagnostic_store, void_annotation, .invalid_void_type, ast.node_span(&tree, void_annotation)) + try testing.expect_equal(void_annotation, ast.param(&tree, void_b).type) + try testing.expect_equal(void_error, type_of(&result, void_a).diagnostic) + try testing.expect_equal(void_error, type_of(&result, void_b).diagnostic) + try expect_kind(&result, ast.function_decl(&tree, void_function).return_type, .void) + storage :: ast.statement_decl(&tree, ast.package_decl(&tree, 5)) + if (storage.type) |annotation| { + _ = try expect_error(&result, &diagnostic_store, annotation, .invalid_void_type, ast.node_span(&tree, annotation)) + } else { + return .expectation_failed + } + holder :: ast.statement_decl(&tree, ast.package_decl(&tree, 8)).initializer + _ = try expect_error(&result, &diagnostic_store, holder, .type_mismatch, ast.node_span(&tree, holder)) + operand :: ast.statement_decl(&tree, ast.package_decl(&tree, 9)).initializer + _ = try expect_error(&result, &diagnostic_store, operand, .invalid_operand, ast.node_span(&tree, operand)) + main_body :: ast.function_decl(&tree, ast.package_decl(&tree, 10)).body + noop_call :: ast.statement_expr(&tree, ast.block_statement(&tree, main_body, 0)) + try expect_kind(&result, noop_call, .void) + bad_call :: ast.statement_expr(&tree, ast.block_statement(&tree, main_body, 1)) + argument :: ast.call_arg(&tree, bad_call, 0) + argument_error :: try expect_error(&result, &diagnostic_store, argument, .type_mismatch, ast.node_span(&tree, argument)) + try testing.expect_equal(argument_error, type_of(&result, bad_call).diagnostic) + rejected [5]usize :: [1, 2, 5, 8, 9] + for rejected |index| { + try testing.expect_equal(TypeKind.invalid, type_of(&result, ast.package_decl(&tree, index)).kind) + } + try expect_kind(&result, storage.initializer, .int) + try expect_kind(&result, ast.package_decl(&tree, 0), .int) + try expect_kind(&result, ast.package_decl(&tree, 11), .string) + try testing.expect_equal(3, code_count(&diagnostic_store, .expected_type)) + try testing.expect_equal(2, code_count(&diagnostic_store, .invalid_void_type)) + try testing.expect_equal(1, code_count(&diagnostic_store, .type_not_value)) + try testing.expect_equal(2, code_count(&diagnostic_store, .type_mismatch)) + try testing.expect_equal(2, code_count(&diagnostic_store, .invalid_operand)) + try testing.expect_equal(10, diagnostics.count(&diagnostic_store)) +} + +reports_nonvoid_fallthrough_without_poisoning_signatures_and_checks_unused_bodies test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `answer :: alias() + `alias :: make + `make proc() int {} + `unused proc() float {} + `broken proc() void { + ` -"x" + `} + `main proc() void { + ` make() + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + answer :: ast.package_decl(&tree, 0) + alias_decl :: ast.package_decl(&tree, 1) + make :: ast.package_decl(&tree, 2) + unused :: ast.package_decl(&tree, 3) + try expect_kind(&result, answer, .int) + try expect_kind(&result, ast.statement_decl(&tree, answer).initializer, .int) + try expect_kind(&result, alias_decl, .function) + try testing.expect_equal(make, type_of(&result, alias_decl).function) + functions [2]ast.NodeId :: [make, unused] + returns [2]TypeKind :: [.int, .float] + for functions |function, index| { + view :: ast.function_decl(&tree, function) + try expect_kind(&result, function, .function) + try testing.expect_equal(function, type_of(&result, function).function) + try expect_kind(&result, view.return_type, returns[index]) + _ = try expect_error(&result, &diagnostic_store, view.body, .missing_return, ast.node_span(&tree, view.body)) + } + broken :: ast.package_decl(&tree, 4) + try expect_kind(&result, broken, .function) + broken_body :: ast.function_decl(&tree, broken).body + bad_operand :: ast.statement_expr(&tree, ast.block_statement(&tree, broken_body, 0)) + _ = try expect_error(&result, &diagnostic_store, bad_operand, .invalid_operand, ast.node_span(&tree, bad_operand)) + main_body :: ast.function_decl(&tree, ast.package_decl(&tree, 5)).body + call :: ast.statement_expr(&tree, ast.block_statement(&tree, main_body, 0)) + try expect_kind(&result, call, .int) + try testing.expect_equal(2, code_count(&diagnostic_store, .missing_return)) + try testing.expect_equal(3, diagnostics.count(&diagnostic_store)) +} + +checks_arguments_even_for_wrong_arity_noncallable_and_chained_callees test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `sink proc(a int, b float) void {} + `noop proc() void {} + `fixed int :: 1 + `main proc() void { + ` sink(1, 2, -"extra") + ` sink() + ` fixed(-"arg") + ` sink(1, "wrong") + ` sink(1, fixed) + ` noop()() + ` sink(2, 3) + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + body :: ast.function_decl(&tree, ast.package_decl(&tree, 3)).body + for usize(0)..usize(2) |index| { + call :: ast.statement_expr(&tree, ast.block_statement(&tree, body, index)) + _ = try expect_error(&result, &diagnostic_store, call, .argument_count_mismatch, ast.node_span(&tree, call)) + } + long_call :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 0)) + extra :: ast.call_arg(&tree, long_call, 2) + _ = try expect_error(&result, &diagnostic_store, extra, .invalid_operand, ast.node_span(&tree, extra)) + try expect_kind(&result, ast.call_arg(&tree, long_call, 0), .int) + try expect_kind(&result, ast.call_arg(&tree, long_call, 1), .float) + noncall :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 2)) + noncall_callee :: ast.call_expr(&tree, noncall).callee + _ = try expect_error(&result, &diagnostic_store, noncall, .not_callable, ast.node_span(&tree, noncall_callee)) + bad_argument :: ast.call_arg(&tree, noncall, 0) + _ = try expect_error(&result, &diagnostic_store, bad_argument, .invalid_operand, ast.node_span(&tree, bad_argument)) + for usize(3)..usize(5) |index| { + call :: ast.statement_expr(&tree, ast.block_statement(&tree, body, index)) + argument :: ast.call_arg(&tree, call, 1) + diagnostic :: try expect_error(&result, &diagnostic_store, argument, .type_mismatch, ast.node_span(&tree, argument)) + try testing.expect_equal(diagnostic, type_of(&result, call).diagnostic) + } + chained :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 5)) + inner :: ast.call_expr(&tree, chained).callee + try expect_kind(&result, inner, .void) + _ = try expect_error(&result, &diagnostic_store, chained, .not_callable, ast.node_span(&tree, inner)) + good_call :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 6)) + try expect_kind(&result, good_call, .void) + try expect_kind(&result, ast.call_arg(&tree, good_call, 1), .float) + try expect_kind(&result, ast.package_decl(&tree, 2), .int) + try testing.expect_equal(2, code_count(&diagnostic_store, .argument_count_mismatch)) + try testing.expect_equal(2, code_count(&diagnostic_store, .not_callable)) + try testing.expect_equal(2, code_count(&diagnostic_store, .invalid_operand)) + try testing.expect_equal(2, code_count(&diagnostic_store, .type_mismatch)) + try testing.expect_equal(8, diagnostics.count(&diagnostic_store)) +} + +terminates_unanchored_cycles_and_keeps_grounded_cycles_typed test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `a :: b + `b :: a + `dependent :: a + `self :: self + `anchored float :: partner + `partner :: anchored + `fixed int :: fixed + `good :: 2 + `numeric :: numeric + 1 + `demand float :: contextual_a + `contextual_a :: contextual_b + `contextual_b :: contextual_a + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + a :: ast.package_decl(&tree, 0) + b :: ast.package_decl(&tree, 1) + dependent :: ast.package_decl(&tree, 2) + shared :: type_of(&result, a).diagnostic + cycle [3]ast.NodeId :: [a, b, dependent] + for cycle |declaration| { + try testing.expect_equal(TypeKind.invalid, type_of(&result, declaration).kind) + try testing.expect_equal(shared, type_of(&result, declaration).diagnostic) + expression :: ast.statement_decl(&tree, declaration).initializer + try testing.expect_equal(TypeKind.invalid, type_of(&result, expression).kind) + try testing.expect_equal(shared, type_of(&result, expression).diagnostic) + } + if (diagnostics.get(&diagnostic_store, shared)) |diagnostic| { + try testing.expect_equal(diagnostics.Code.cannot_infer_type, diagnostic.code) + a_name :: ast.statement_decl(&tree, a).name + b_name :: ast.statement_decl(&tree, b).name + try testing.expect( + (diagnostic.span.start == a_name.start and diagnostic.span.end == a_name.end) + or (diagnostic.span.start == b_name.start and diagnostic.span.end == b_name.end), + ) + } else { + return .expectation_failed + } + self :: ast.package_decl(&tree, 3) + self_error :: try expect_error(&result, &diagnostic_store, self, .cannot_infer_type, ast.statement_decl(&tree, self).name) + try testing.expect(self_error != shared) + try testing.expect_equal(self_error, type_of(&result, ast.statement_decl(&tree, self).initializer).diagnostic) + for usize(4)..usize(6) |index| { + declaration :: ast.package_decl(&tree, index) + try expect_kind(&result, declaration, .float) + try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .float) + } + try expect_kind(&result, ast.package_decl(&tree, 6), .int) + try expect_kind(&result, ast.statement_decl(&tree, ast.package_decl(&tree, 6)).initializer, .int) + try expect_kind(&result, ast.package_decl(&tree, 7), .int) + numeric :: ast.package_decl(&tree, 8) + try expect_kind(&result, numeric, .int) + try expect_kind(&result, ast.statement_decl(&tree, numeric).initializer, .int) + for usize(9)..usize(12) |index| { + declaration :: ast.package_decl(&tree, index) + try expect_kind(&result, declaration, .float) + try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .float) + } + try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) +} + +preserves_existing_syntax_name_and_duplicate_ids_while_checking_siblings test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `@ broken + `main proc(a, b Missing) void { + ` empty :: () + ` bad :: absent + -"sibling" + ` unknown(-"argument") + ` valid float :: later + ` later := 1 + ` discarded(1 2) + `} + `same :: 1 + `same :: -"initializer" + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store) + defer sema.deinit(&resolution) + previous_count :: diagnostics.count(&diagnostic_store) + function :: ast.package_decl(&tree, 1) + body :: ast.function_decl(&tree, function).body + invalid_decl :: ast.package_decl(&tree, 0) + empty_decl :: ast.block_statement(&tree, body, 0) + empty :: ast.statement_decl(&tree, empty_decl).initializer + invalid_statement :: ast.block_statement(&tree, body, 5) + bad_decl :: ast.block_statement(&tree, body, 1) + bad_expr :: ast.statement_decl(&tree, bad_decl).initializer + bad :: ast.binary_expr(&tree, bad_expr) + unknown_call :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 2)) + unknown_callee :: ast.call_expr(&tree, unknown_call).callee + duplicate :: ast.package_decl(&tree, 3) + a :: ast.function_param(&tree, function, 0) + b :: ast.function_param(&tree, function, 1) + shared_type :: ast.param(&tree, a).type + try testing.expect_equal(shared_type, ast.param(&tree, b).type) + result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store) + defer deinit(&result) + + syntax_nodes [3]ast.NodeId :: [invalid_decl, empty, invalid_statement] + for syntax_nodes |id| { + try testing.expect_equal(TypeKind.invalid, type_of(&result, id).kind) + try testing.expect_equal(ast.invalid(&tree, id).diagnostic, type_of(&result, id).diagnostic) + } + try testing.expect_equal(type_of(&result, empty).diagnostic, type_of(&result, empty_decl).diagnostic) + resolved_invalid [4]ast.NodeId :: [shared_type, bad.left, unknown_callee, duplicate] + for resolved_invalid |id| { + try testing.expect_equal(sema.BindingKind.invalid, sema.binding(&resolution, id).kind) + try testing.expect_equal(TypeKind.invalid, type_of(&result, id).kind) + try testing.expect_equal(sema.binding(&resolution, id).data.diagnostic, type_of(&result, id).diagnostic) + } + try testing.expect_equal(type_of(&result, shared_type).diagnostic, type_of(&result, a).diagnostic) + try testing.expect_equal(type_of(&result, shared_type).diagnostic, type_of(&result, b).diagnostic) + try testing.expect_equal(type_of(&result, bad.left).diagnostic, type_of(&result, bad_expr).diagnostic) + try testing.expect_equal(type_of(&result, bad.left).diagnostic, type_of(&result, bad_decl).diagnostic) + try testing.expect_equal(type_of(&result, unknown_callee).diagnostic, type_of(&result, unknown_call).diagnostic) + _ = try expect_error(&result, &diagnostic_store, bad.right, .invalid_operand, ast.node_span(&tree, bad.right)) + argument :: ast.call_arg(&tree, unknown_call, 0) + _ = try expect_error(&result, &diagnostic_store, argument, .invalid_operand, ast.node_span(&tree, argument)) + duplicate_initializer :: ast.statement_decl(&tree, duplicate).initializer + _ = try expect_error(&result, &diagnostic_store, duplicate_initializer, .invalid_operand, ast.node_span(&tree, duplicate_initializer)) + for usize(3)..usize(5) |index| { + declaration :: ast.block_statement(&tree, body, index) + try expect_kind(&result, declaration, .float) + try expect_kind(&result, ast.statement_decl(&tree, declaration).initializer, .float) + } + try expect_kind(&result, ast.package_decl(&tree, 2), .int) + try testing.expect_equal(3, code_count(&diagnostic_store, .unknown_name)) + try testing.expect_equal(1, code_count(&diagnostic_store, .duplicate_name)) + try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_operand)) + try testing.expect_equal(previous_count + 3, diagnostics.count(&diagnostic_store)) +}