From c6a56b7e79dc509c505049898049e7f03a51cc6a Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 10 Oct 2026 17:21:43 +0200 Subject: [PATCH] feat: execute fixed arrays and indexed access --- README.md | 16 +- TODO.md | 3 + programs/array_sum.hon | 12 + source/ast/ast.hon | 95 +++- source/ast/ast.test.hon | 32 ++ source/ast/renderer/renderer.hon | 6 + source/ast/renderer/renderer.test.hon | 33 ++ source/diagnostics/diagnostics.hon | 18 +- source/interpreter/interpreter.hon | 253 ++++++++- source/interpreter/interpreter.test.hon | 715 +++++++++++++++++++++++- source/ir/ir.hon | 45 ++ source/lexer/scan.hon | 1 + source/lexer/scan.test.hon | 19 + source/lexer/token.hon | 1 + source/lower/lower.hon | 216 ++++++- source/lower/lower.test.hon | 466 +++++++++++++-- source/main.hon | 46 +- source/parser/parse.hon | 268 +++++++-- source/parser/parse.test.hon | 198 +++++++ source/sema/check/check.hon | 639 ++++++++++++++++++--- source/sema/check/check.test.hon | 471 ++++++++++++++++ source/sema/resolve.hon | 25 +- source/sema/resolve.test.hon | 152 +++++ 23 files changed, 3474 insertions(+), 256 deletions(-) create mode 100644 TODO.md create mode 100644 programs/array_sum.hon create mode 100644 source/ast/renderer/renderer.test.hon diff --git a/README.md b/README.md index 3524bbd..91cdf9b 100644 --- a/README.md +++ b/README.md @@ -49,19 +49,25 @@ function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. ca `name = expression` updates a local `:=` binding. `::` bindings, parameters, named functions, builtin types and globals are not assignment targets. the established binding type is preserved: assignment inputs do not widen or infer that type, though numeric literals can adopt its float context. function-valued locals accept replacement functions with the same parameter and result types. +fixed arrays use `[N]T` for readonly elements and `[N]mut T` for mutable elements; literals use `[a, b]`, including empty, nested and multiline forms with trailing commas. `values [4]mut i64 := [1, 2, 3, 4]` permits `values[2] = 10`. writable elements require a mutable local binding and `mut` at every traversed array level; parameters and globals remain readonly. whole-array assignment only requires a mutable binding, but source and destination types must match recursively, including counts and element mutability. initializations, assignments, arguments and returns copy arrays by value, including nested rows. + +array lengths currently support nonnegative integer literals, unary signs, `+`, `-`, `*` and package-level immutable integer constants; local constants and other compile-time expressions are not implemented. `[_]` infers a binding's array length from its initializer. element annotations require concrete supported value types: use `i64` or `f64` rather than the `int` or `float` constraints. an unannotated array literal has readonly elements. + +indices require `usize`; integer literals and flexible inferred values receive this context, while fixed signed integers do not convert implicitly. out-of-bounds access traps before loading or storing, including for empty arrays. reads evaluate the base before the index; indexed assignment evaluates its right-hand side before the destination, each once. leading nested assignment targets follow Bro's declaration ambiguity: write `(matrix[1])[0] = 9`, not `matrix[1][0] = 9`. [programs/array_sum.hon](programs/array_sum.hon) uses a `usize` range bound to index, mutate and sum an array; `./build/honey run programs/array_sum.hon` prints `17`. + `while` repeats a block or single statement while its `bool` condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as `if`. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry. -`for 0..10 |i| { ... }` iterates an ascending, upper-exclusive integer range; `..=` includes the upper bound. a capture is required: `|i|` binds an immutable `int` in the body scope, while `|_|` discards it. bounds resolve in the enclosing scope and are evaluated left-to-right once, so changing a bound variable in the body does not change the range. reversed ranges are empty; equal bounds produce zero iterations with `..` and one with `..=`. inclusive ranges terminate safely at the maximum integer. +`for 0..10 |i| { ... }` iterates an ascending, upper-exclusive integer range; `..=` includes the upper bound. a capture is required: `|i|` binds an immutable value with the bounds' integer family (`int` or `usize`) in the body scope, while `|_|` discards it. bounds resolve in the enclosing scope and are evaluated left-to-right once, so changing a bound variable in the body does not change the range. reversed ranges are empty; equal bounds produce zero iterations with `..` and one with `..=`. inclusive ranges terminate safely at the maximum integer. range bounds that are compound or unary expressions require their own parentheses, such as `(-2)..(limit + 1)`. a braceless body also requires parentheses around the entire iterable: `for (0..10) |i| total = total + i`. current execution supports direct integer ranges only; first-class range values, collection iteration, pointer captures and index captures remain unimplemented. [programs/range_loop.hon](programs/range_loop.hon) skips `5` while summing `0..10`; `./build/honey run programs/range_loop.hon` prints `40`. `break` exits the nearest enclosing `for` or `while`; `continue` restarts a `while` condition or advances a `for` range through its guarded update. both are operand-free statements permitted within loop bodies and nested conditional arms; labels and values are unsupported. controls outside loops produce keyword-spanned diagnostics, including in untaken or unreachable code, and trap only if reached. code after a transfer is still resolved and type-checked but does not execute on that path. body locals are reset on re-entry after `continue`; loop transfers do not guarantee a function return. both controls lower to existing IR jumps, without new interpreter instructions or loop frames. [programs/loop_control.hon](programs/loop_control.hon) skips an iteration and exits early; `./build/honey run programs/loop_control.hon` prints `12`. -name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `bool`, `string`, `void`); sibling scopes may independently reuse names. +name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `i64`, `f64`, `usize`, `bool`, `string`, `void`); sibling 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 (`@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. +type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types and interned array descriptors; query with `check.type_of` and `check.array_type`, 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 support builtin types and fixed arrays; type names are not ordinary values. `void` is valid as a function result, not as a parameter or stored value. numeric typing follows 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; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid; division involving fixed `int` values requires explicit conversion. @@ -69,6 +75,8 @@ type errors carry source spans and diagnostic identities; invalid operands propa `lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; loops use condition/body/exit blocks and backedges. short-circuit expressions use branches and synthetic boolean local slots to merge results; call arguments are packed in the block reached after evaluating them. local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter. -`interpreter.run` consumes only IR and returns a value or a diagnostic trap. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. integers use signed 64-bit storage with checked arithmetic; floats use 64-bit IEEE arithmetic and comparisons, including NaN behavior. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes. +array IR retains recursive element types, counts and mutability. typed aggregate construction, element addresses and load/store instructions use flattened storage reserved per invocation; nested copies and indexed accesses reuse that storage without per-iteration allocation. addresses are internal IR values, not source-language pointers. + +`interpreter.run` consumes only IR and returns an `Outcome` containing a value or diagnostic trap. returned arrays own their escaped cell storage; release every outcome with `interpreter.deinit` before releasing its module. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. array descriptors also borrow the module. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. `int`/`i64` use signed 64-bit storage, `usize` uses unsigned 64-bit storage, both with checked arithmetic; `float`/`f64` use 64-bit IEEE arithmetic and comparisons, including NaN behavior. `usize` supports `+`, `-`, `*` and comparisons, but not unary negation or division. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes. recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation and compound assignment remain unimplemented. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e1de3ac --- /dev/null +++ b/TODO.md @@ -0,0 +1,3 @@ +- [ ] consider whether numeric conversion semantics are properly tuned. + - e.g. are we disallowing implicit conversion between types where loss of precission or wrapping could/would occur? + - should we do that or should we not? \ No newline at end of file diff --git a/programs/array_sum.hon b/programs/array_sum.hon new file mode 100644 index 0000000..3eb4064 --- /dev/null +++ b/programs/array_sum.hon @@ -0,0 +1,12 @@ +main proc() int { + values [4]mut i64 := [1, 2, 3, 4] + values[2] = 10 + + total := 0 + count usize :: 4 + for 0..count |index| { + total = total + values[index] + } + + return total +} diff --git a/source/ast/ast.hon b/source/ast/ast.hon index 3293ec5..b13790a 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -1,15 +1,15 @@ #! grammar: -#! * type ..... -> identifier +#! * type ..... -> identifier | '[' (expr | '_') ']' ['mut'] type #! * binding ... -> :: | := #! * literal .. -> int | float | string | 'true' | 'false' #! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or' -#! * expr ..... -> identifier | literal | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr ('..' | '..=') expr +#! * expr ..... -> identifier | literal | type | '[' [expr {',' expr} [',']] ']' | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr ('..' | '..=') expr #! * decl ..... -> identifier [type] binding expr #! * block .... -> '{' {stmt} '}' #! * arm ...... -> block | stmt (single statements become canonical blocks) -#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm +#! * stmt ..... -> decl | expr | expr '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm #! braceless guards/iterables require parentheses unless they are calls; else binds to the nearest if. -#! precedence (tightest first): calls, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses. +#! precedence (tightest first): calls/indices, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses. #! binary operators associate left; unary operators associate right. import "@std/debug" @@ -40,6 +40,9 @@ ExprKind :: enum { binary call range + array_type + array_literal + index invalid } @@ -86,6 +89,9 @@ NodeKind :: enum { expr_binary, expr_call, expr_range, + expr_array_type, + expr_array_literal, + expr_index, stmt_expr, stmt_decl, stmt_return, @@ -181,6 +187,19 @@ CallExprView :: struct { callee NodeId } +ArrayTypeView :: struct { + span Span + count ?NodeId + element NodeId + mutable bool +} + +IndexView :: struct { + span Span + base NodeId + index NodeId +} + ReturnView :: struct { span Span value ?NodeId @@ -336,6 +355,37 @@ add_call proc( }, span) } +add_array_type proc( + builder @mut Builder, span Span, count ?NodeId, element NodeId, mutable bool, +) NodeId ! mem.AllocError { + raw_count :: if (count) |id| id else no_node + flag :: if (mutable) u32(1) else u32(0) + return try add_node(builder, Node{ + kind = .expr_array_type, + main = NodeData{ extra = flag }, + data0 = NodeData{ node_id = raw_count }, + data1 = NodeData{ node_id = element }, + }, span) +} + +add_array_literal proc(builder @mut Builder, span Span, elements []NodeId) NodeId ! mem.AllocError { + start :: builder.storage.extra.items.len + for (elements) |element| _ = try add_extra(builder, u32(element)) + return try add_node(builder, Node{ + kind = .expr_array_literal, + data0 = NodeData{ extra = u32(start) }, + data1 = NodeData{ extra = u32(builder.storage.extra.items.len) }, + }, span) +} + +add_index proc(builder @mut Builder, span Span, base, index NodeId) NodeId ! mem.AllocError { + return try add_node(builder, Node{ + kind = .expr_index, + data0 = NodeData{ node_id = base }, + data1 = NodeData{ node_id = index }, + }, span) +} + add_expression_stmt proc(builder @mut Builder, expression NodeId) NodeId ! mem.AllocError { return try add_node(builder, Node{ kind = .stmt_expr, @@ -710,6 +760,9 @@ expression_kind proc(kind NodeKind) ExprKind { .expr_binary: .binary .expr_call: .call .expr_range: .range + .expr_array_type: .array_type + .expr_array_literal: .array_literal + .expr_index: .index .invalid_expr: .invalid else: unreachable } @@ -769,6 +822,40 @@ call_arg proc(tree @Tree, id NodeId, index usize) NodeId { return NodeId(tree.storage.extra.items[usize(node.data0.extra) + index]) } +array_type proc(tree @Tree, id NodeId) ArrayTypeView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_array_type) + return ArrayTypeView{ + span = get_span(&tree.storage, id), + count = maybe_node_id(u32(node.data0.node_id)), + element = node.data1.node_id, + mutable = node.main.extra != 0, + } +} + +array_element_count proc(tree @Tree, id NodeId) usize { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_array_literal) + return usize(node.data1.extra - node.data0.extra) +} + +array_element proc(tree @Tree, id NodeId, index usize) NodeId { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_array_literal) + debug.assert(index < usize(node.data1.extra - node.data0.extra)) + return NodeId(tree.storage.extra.items[usize(node.data0.extra) + index]) +} + +index_expr proc(tree @Tree, id NodeId) IndexView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_index) + return IndexView{ + span = get_span(&tree.storage, id), + base = node.data0.node_id, + index = node.data1.node_id, + } +} + node_span proc(tree @Tree, id NodeId) Span { return get_span(&tree.storage, id) } diff --git a/source/ast/ast.test.hon b/source/ast/ast.test.hon index 676700f..05a6299 100644 --- a/source/ast/ast.test.hon +++ b/source/ast/ast.test.hon @@ -32,3 +32,35 @@ exposes_typed_declaration_views test { try testing.expect_equal(ExprKind.literal_int, expr_kind(&tree, statement.initializer)) try testing.expect_equal("value", source_text(&tree, statement.name)) } + +exposes_array_type_literal_and_index_views test { + builder := init_builder(mem.c_allocator, "") + defer deinit_builder(&builder) + span :: Span{ start = 0, end = 0 } + count :: try add_literal(&builder, .int, span) + element :: try add_identifier(&builder, span) + row :: try add_array_type(&builder, span, count, element, false) + matrix :: try add_array_type(&builder, span, null, row, true) + elements [2]NodeId :: [count, element] + literal :: try add_array_literal(&builder, span, elements[..]) + empty :: try add_array_literal(&builder, span, &[]) + indexed :: try add_index(&builder, span, literal, count) + package :: try add_package(&builder, span, &[]) + tree := finish(&builder, package) + defer deinit(&tree) + try testing.expect_equal(ExprKind.array_type, expr_kind(&tree, matrix)) + try testing.expect_equal(null, array_type(&tree, matrix).count) + try testing.expect(array_type(&tree, matrix).mutable) + try testing.expect_equal(row, array_type(&tree, matrix).element) + try testing.expect_equal(count, array_type(&tree, row).count?) + try testing.expect(!array_type(&tree, row).mutable) + try testing.expect_equal(element, array_type(&tree, row).element) + try testing.expect_equal(ExprKind.array_literal, expr_kind(&tree, literal)) + try testing.expect_equal(2, array_element_count(&tree, literal)) + try testing.expect_equal(count, array_element(&tree, literal, 0)) + try testing.expect_equal(element, array_element(&tree, literal, 1)) + try testing.expect_equal(0, array_element_count(&tree, empty)) + try testing.expect_equal(ExprKind.index, expr_kind(&tree, indexed)) + try testing.expect_equal(literal, index_expr(&tree, indexed).base) + try testing.expect_equal(count, index_expr(&tree, indexed).index) +} diff --git a/source/ast/renderer/renderer.hon b/source/ast/renderer/renderer.hon index ae0f707..ab2166f 100644 --- a/source/ast/renderer/renderer.hon +++ b/source/ast/renderer/renderer.hon @@ -132,6 +132,12 @@ render_statement proc( try append_text(renderer, indent) try append_text(renderer, "declaration ") try append_text(renderer, ast.source_text(tree, statement.name)) + if (statement.type) |type| { + try append_text(renderer, " ") + try append_text(renderer, ast.source_text(tree, ast.node_span(tree, type))) + } + try append_text(renderer, if (statement.mutability == .constant) " :: " else " := ") + try append_text(renderer, ast.source_text(tree, ast.node_span(tree, statement.initializer))) try append_text(renderer, "\n") } diff --git a/source/ast/renderer/renderer.test.hon b/source/ast/renderer/renderer.test.hon new file mode 100644 index 0000000..405593f --- /dev/null +++ b/source/ast/renderer/renderer.test.hon @@ -0,0 +1,33 @@ +import "@std/mem" +import "@std/testing" +import "@source/diagnostics" +import "@source/strpool" +import "@source/lexer" +import "@source/parser" +import "@source/ast" + +renders_array_annotations_literals_and_indexed_assignments 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 :: + `data [2]mut i64 := [1, 2] + `main proc() i64 { + ` (data)[0] = 9 + ` return data[0] + `} + 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) + renderer := init(mem.c_allocator) + defer deinit(&renderer) + try render(&renderer, &tree) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + try testing.expect_equal( + "package\n declaration data [2]mut i64 := [1, 2]\n function main\n assignment (data)[0] = 9\n return data[0]\n", + renderer.bytes.items, + ) +} diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 6bbde15..b582a61 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -37,6 +37,14 @@ Code :: enum { range_value_not_supported, range_pointer_capture, range_index_capture, + invalid_array_length, + array_length_mismatch, + invalid_array_element_type, + array_size_overflow, + invalid_index_type, + not_indexable, + invalid_index_assignment, + index_out_of_bounds, missing_entry, invalid_entry, literal_out_of_range, @@ -55,7 +63,7 @@ Diagnostic :: struct { #! owns diagnostic storage and retains its allocator; release with deinit. Store :: struct { list arraylist.ArrayList(Diagnostic) } -CapacityError :: enum { diagnostic_limit_reached } +CapacityError :: enum { diagnostic_limit_reached, storage_limit_reached } #! retains `allocator` for added diagnostics; the caller owns the returned Store. init proc(allocator mem.Allocator) Store { @@ -112,6 +120,14 @@ messages :: $enummap.init(Code, []u8, { range_value_not_supported = "first-class range values are not implemented", range_pointer_capture = "range loops do not support pointer captures", range_index_capture = "range loops do not support index captures", + invalid_array_length = "array length must be a nonnegative supported compile-time integer", + array_length_mismatch = "array literal length does not match its type", + invalid_array_element_type = "array element annotations require a supported concrete value type", + array_size_overflow = "array storage size exceeds the supported address space", + invalid_index_type = "array index must have type usize", + not_indexable = "indexing is currently implemented for fixed arrays only", + invalid_index_assignment = "indexed assignment requires a writable array element", + index_out_of_bounds = "index out of bounds", missing_entry = "program requires a main function", invalid_entry = "main must be a function with no parameters", literal_out_of_range = "numeric literal is outside its runtime type's range", diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 99c516e..cffa549 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -6,6 +6,17 @@ import "@source/diagnostics" Outcome :: struct { value ir.Value = ir.Value{} trap ?diagnostics.DiagnosticId = null + allocator ?mem.Allocator = null + owned_elements ?[]mut ir.Value = null +} + +#! release an escaped aggregate with its retained allocator; scalar and trap outcomes need no allocation. +#! strings and array descriptors borrow the module, which must outlive this outcome. +deinit proc(outcome @mut Outcome) void { + if (outcome.owned_elements) |elements| mem.free(outcome.allocator?, elements) + outcome.owned_elements = null + outcome.allocator = null + outcome.value = ir.Value{} } @hide:file @@ -23,6 +34,15 @@ GlobalState :: struct { value ir.Value = ir.Value{} } +@hide:file +Address :: struct { + type ir.Type + array_type ir.ArrayId = ir.no_array + cells []mut ir.Value + scalar ?@mut ir.Value = null + initialized ?@mut bool = null +} + @hide:file Frame :: struct { function ir.FunctionId @@ -32,10 +52,12 @@ Frame :: struct { global ?usize = null locals []mut Slot values []mut ir.Value + aggregate []mut ir.Value + addresses []mut Address } -#! executes only typed IR; invocation and global state are temporary and released on every exit. -#! returned string IDs resolve through `module`; runtime source errors are traps, not infrastructure errors. +#! executes typed IR with invocation/global storage released on every exit. +#! array results own flat storage released by `deinit`; string IDs and shapes resolve through `module`. run proc( allocator mem.Allocator, module @ir.Module, @@ -51,6 +73,22 @@ run proc( globals :: try mem.alloc(GlobalState, allocator, module.globals.items.len) defer mem.free(allocator, globals) mem.set(globals, GlobalState{}) + global_cells usize := 0 + for (module.globals.items) |global| { + if (global.type != .array) continue + cells :: module.arrays.items[usize(global.array_type)].cells + if (cells > maxval!(usize) - global_cells) return .storage_limit_reached + global_cells += cells + } + aggregate_globals :: try mem.alloc(ir.Value, allocator, global_cells) + defer mem.free(allocator, aggregate_globals) + global_offset usize := 0 + for (module.globals.items) |global, i| { + if (global.type != .array) continue + cells :: module.arrays.items[usize(global.array_type)].cells + globals[i].value = array_view(global.array_type, aggregate_globals[global_offset..global_offset + cells]) + global_offset += cells + } frames := arraylist.init(Frame, allocator) defer { for (frames.items) |frame| free_frame(allocator, frame) @@ -81,16 +119,25 @@ run proc( value = frames.items[frame_index].values[usize(terminator.value)] } completed :: frames.items[frame_index] - _ = arraylist.pop(&frames) - free_frame(allocator, completed) + # transfer leaves while the completed frame still owns its backing. if completed.global |global_index| { - globals[global_index].value = value + copy_value(&globals[global_index].value, value) globals[global_index].status = .ready } - if (frames.items.len == 0) return Outcome{ value = value } - if completed.destination != ir.no_value { - frames.items[frames.items.len - 1].values[usize(completed.destination)] = value + if (frames.items.len == 1) { + if (value.type == .array) { + escaped :: try mem.alloc(ir.Value, allocator, value.data.elements.len) + memcopy!(escaped, value.data.elements) + value.data.elements = escaped + return Outcome{ value = value, allocator = allocator, owned_elements = escaped } + } + return Outcome{ value = value } } + if completed.destination != ir.no_value { + copy_value(&frames.items[frame_index - 1].values[usize(completed.destination)], value) + } + _ = arraylist.pop(&frames) + free_frame(allocator, completed) continue } @@ -107,10 +154,9 @@ run proc( value = local.value } .store_local: { - frames.items[frame_index].locals[usize(instruction.slot)] = Slot{ - initialized = true, - value = frames.items[frame_index].values[usize(instruction.left)], - } + local :: &frames.items[frame_index].locals[usize(instruction.slot)] + copy_value(&local.value, frames.items[frame_index].values[usize(instruction.left)]) + local.initialized = true continue } .assign_local: { @@ -118,7 +164,7 @@ run proc( if (!local.initialized) { return try runtime_trap(diagnostic_store, .write_before_initialization, instruction.span) } - local.value = frames.items[frame_index].values[usize(instruction.left)] + copy_value(&local.value, frames.items[frame_index].values[usize(instruction.left)]) continue } .reset_local: { @@ -152,13 +198,107 @@ run proc( # frame indices survive stack growth; pointers into frames.items do not. for (0..instruction.arguments_count) |argument_index| { argument :: block.arguments.items[instruction.arguments_start + argument_index] - frames.items[frame_index + 1].locals[argument_index] = Slot{ - initialized = true, - value = frames.items[frame_index].values[usize(argument)], + local :: &frames.items[frame_index + 1].locals[argument_index] + copy_value(&local.value, frames.items[frame_index].values[usize(argument)]) + local.initialized = true + } + continue + } + .array_literal: { + destination :: &frames.items[frame_index].values[usize(instruction.destination)] + offset usize := 0 + for (0..instruction.arguments_count) |i| { + operand :: block.arguments.items[instruction.arguments_start + i] + element :: frames.items[frame_index].values[usize(operand)] + if (element.type == .array) { + count :: element.data.elements.len + memcopy!(destination.data.elements[offset..offset + count], element.data.elements) + offset += count + } else { + destination.data.elements[offset] = element + offset += 1 } } continue } + .index: { + base :: frames.items[frame_index].values[usize(instruction.left)] + index :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned + shape :: module.arrays.items[usize(base.array_type)] + if (index >= u64(shape.len)) { + return try runtime_trap(diagnostic_store, .index_out_of_bounds, instruction.span) + } + value = indexed_value(module, base.array_type, base.data.elements, usize(index)) + } + .address_local: { + local :: &frames.items[frame_index].locals[usize(instruction.slot)] + address := Address{ + type = instruction.pointee, array_type = instruction.array_type, + cells = mem.empty(ir.Value), initialized = &local.initialized, + } + if (address.type == .array) address.cells = local.value.data.elements + else address.scalar = &local.value + frames.items[frame_index].addresses[usize(instruction.destination)] = address + value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } } + } + .address_global: { + global_index :: usize(instruction.slot) + match globals[global_index].status { + .initializing: return try runtime_trap(diagnostic_store, .initialization_cycle, instruction.span) + .uninitialized: { + frames.items[frame_index].next_instruction -= 1 + try push_frame(allocator, module, &frames, module.globals.items[global_index].initializer, ir.no_value, global_index) + globals[global_index].status = .initializing + continue + } + .ready: {} + } + global :: &globals[global_index] + address := Address{ + type = instruction.pointee, array_type = instruction.array_type, + cells = mem.empty(ir.Value), + } + if (address.type == .array) address.cells = global.value.data.elements + else address.scalar = &global.value + frames.items[frame_index].addresses[usize(instruction.destination)] = address + value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } } + } + .index_address: { + base_id :: frames.items[frame_index].values[usize(instruction.left)].data.address + base :: frames.items[frame_index].addresses[usize(base_id)] + index :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned + shape :: module.arrays.items[usize(base.array_type)] + if (index >= u64(shape.len)) { + return try runtime_trap(diagnostic_store, .index_out_of_bounds, instruction.span) + } + width :: element_cells(module, shape) + start :: usize(index) * width + address := Address{ + type = instruction.pointee, array_type = instruction.array_type, + cells = base.cells[start..start + width], initialized = base.initialized, + } + if (address.type != .array) address.scalar = &address.cells[0] + frames.items[frame_index].addresses[usize(instruction.destination)] = address + value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } } + } + .load_address, .store_address: { + id :: frames.items[frame_index].values[usize(instruction.left)].data.address + address :: frames.items[frame_index].addresses[usize(id)] + if address.initialized |initialized| { + if (!initialized^) { + code :: if (instruction.op == .load_address) diagnostics.Code.read_before_initialization else diagnostics.Code.write_before_initialization + return try runtime_trap(diagnostic_store, code, instruction.span) + } + } + if (instruction.op == .load_address) { + value = if (address.type == .array) array_view(address.array_type, address.cells) else address.scalar?^ + } else { + source :: frames.items[frame_index].values[usize(instruction.right)] + if (address.type == .array) memcopy!(address.cells, source.data.elements) + else address.scalar?^ = source + continue + } + } .negate: { operand :: frames.items[frame_index].values[usize(instruction.left)] if instruction.type == .float { @@ -187,6 +327,13 @@ run proc( else: unreachable } value = ir.Value{ type = .float, data = ir.ValueData{ real = result } } + } else if instruction.type == .usize { + result :: checked_unsigned(instruction.op, left.data.unsigned, right.data.unsigned) + if result |unsigned| { + value = ir.Value{ type = .usize, data = ir.ValueData{ unsigned = unsigned } } + } else { + return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span) + } } else { result :: checked_integer(instruction.op, left.data.integer, right.data.integer) if result |result_integer| { @@ -210,6 +357,15 @@ run proc( .greater_equal: left.data.integer >= right.data.integer else: unreachable } + .usize: result = match instruction.op { + .equal: left.data.unsigned == right.data.unsigned + .not_equal: left.data.unsigned != right.data.unsigned + .less: left.data.unsigned < right.data.unsigned + .less_equal: left.data.unsigned <= right.data.unsigned + .greater: left.data.unsigned > right.data.unsigned + .greater_equal: left.data.unsigned >= right.data.unsigned + else: unreachable + } .float: result = match instruction.op { .equal: left.data.real == right.data.real .not_equal: left.data.real != right.data.real @@ -230,7 +386,7 @@ run proc( } } if instruction.destination != ir.no_value { - frames.items[frame_index].values[usize(instruction.destination)] = value + copy_value(&frames.items[frame_index].values[usize(instruction.destination)], value) } } unreachable @@ -251,6 +407,17 @@ push_frame proc( mem.set(locals, Slot{}) values :: try mem.alloc(ir.Value, allocator, signature.value_count) errdefer mem.free(allocator, values) + aggregate :: try mem.alloc(ir.Value, allocator, signature.aggregate_cells) + errdefer mem.free(allocator, aggregate) + address_count usize :: if (signature.has_addresses) signature.value_count else 0 + addresses :: try mem.alloc(Address, allocator, address_count) + errdefer mem.free(allocator, addresses) + for (signature.array_storage.items) |storage| { + cells :: module.arrays.items[usize(storage.array_type)].cells + value :: array_view(storage.array_type, aggregate[storage.offset..storage.offset + cells]) + if (storage.local) locals[usize(storage.index)].value = value + else values[usize(storage.index)] = value + } # typed IR assigns each temporary before use; only local slots need initialization flags. try arraylist.append(frames, Frame{ function = function, @@ -259,6 +426,8 @@ push_frame proc( global = global, locals = locals, values = values, + aggregate = aggregate, + addresses = addresses, }) } @@ -266,6 +435,8 @@ push_frame proc( free_frame proc(allocator mem.Allocator, frame Frame) void { mem.free(allocator, frame.locals) mem.free(allocator, frame.values) + mem.free(allocator, frame.aggregate) + mem.free(allocator, frame.addresses) } @hide:file @@ -305,3 +476,51 @@ checked_integer proc(op ir.Op, left i64, right i64) ?i64 { else: unreachable } } + +@hide:file +array_view proc(shape ir.ArrayId, cells []mut ir.Value) ir.Value { + return ir.Value{ type = .array, array_type = shape, data = ir.ValueData{ elements = cells } } +} + +@hide:file +copy_value proc(destination @mut ir.Value, source ir.Value) void { + if (source.type == .array) { + # aggregate destinations already own disjoint fixed segments; never install an alias. + memcopy!(destination.data.elements, source.data.elements) + } else { + destination^ = source + } +} + +@hide:file +element_cells proc(module @ir.Module, shape ir.ArrayType) usize { + return if (shape.element == .array) module.arrays.items[usize(shape.element_array)].cells else 1 +} + +@hide:file +indexed_value proc(module @ir.Module, shape_id ir.ArrayId, cells []mut ir.Value, index usize) ir.Value { + shape :: module.arrays.items[usize(shape_id)] + if (shape.element != .array) return cells[index] + width :: element_cells(module, shape) + start :: index * width + return array_view(shape.element_array, cells[start..start + width]) +} + +@hide:file +checked_unsigned proc(op ir.Op, left u64, right u64) ?u64 { + match op { + .add: { + if (left > maxval!(u64) - right) return null + return left + right + } + .subtract: { + if (left < right) return null + return left - right + } + .multiply: { + if (right != 0 and left > divtrunc!(maxval!(u64), right)) return null + return left * right + } + else: unreachable + } +} diff --git a/source/interpreter/interpreter.test.hon b/source/interpreter/interpreter.test.hon index d2af5a3..2a70d2e 100644 --- a/source/interpreter/interpreter.test.hon +++ b/source/interpreter/interpreter.test.hon @@ -16,6 +16,7 @@ add_function proc( errdefer { arraylist.deinit(&function.parameters) arraylist.deinit(&function.locals) + arraylist.deinit(&function.array_storage) for (function.blocks.items) |block| { owned := block arraylist.deinit(&owned.instructions) @@ -37,6 +38,7 @@ add_function proc( @hide:file emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! mem.AllocError { + if (instruction.type == .address) module.functions.items[usize(function)].has_addresses = true try arraylist.append(&module.functions.items[usize(function)].blocks.items[0].instructions, instruction) } @@ -110,7 +112,8 @@ repeated_calls_have_fresh_parameters_locals_and_temporaries test { try emit(&module, main, function_value(6, noop)) try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(7), left = ir.ValueId(6) }) return_value(&module, main, ir.ValueId(5)) - outcome :: try run(mem.c_allocator, &module, &store) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(i64(42), outcome.value.data.integer) @@ -138,11 +141,13 @@ lazy_globals_follow_forward_dependencies_and_reset_between_runs test { try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(1), slot = 0 }) try emit(&module, main, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) }) return_value(&module, main, ir.ValueId(2)) - initial :: try run(mem.c_allocator, &module, &store) + initial := try run(mem.c_allocator, &module, &store) + defer deinit(&initial) try testing.expect(initial.trap == null) try testing.expect_equal(i64(42), initial.value.data.integer) module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = integer(0, 30) - repeated :: try run(mem.c_allocator, &module, &store) + repeated := try run(mem.c_allocator, &module, &store) + defer deinit(&repeated) try testing.expect(repeated.trap == null) try testing.expect_equal(i64(62), repeated.value.data.integer) } @@ -176,7 +181,8 @@ function_aliases_and_borrowed_string_values_execute test { try emit(&module, main, ir.Instruction{ op = .load_global, type = .function, destination = ir.ValueId(0), slot = 0 }) try emit(&module, main, ir.Instruction{ op = .call, type = .string, destination = ir.ValueId(1), left = ir.ValueId(0) }) return_value(&module, main, ir.ValueId(1)) - outcome :: try run(mem.c_allocator, &module, &store) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.string, outcome.value.type) try testing.expect_equal([]u8, text, ir.string_bytes(&module, outcome.value.data.string)) @@ -193,7 +199,8 @@ premature_local_reads_and_global_cycles_report_instruction_spans test { module.entry = main try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0, span = span }) return_value(&module, main, ir.ValueId(0)) - premature :: try run(mem.c_allocator, &module, &store) + premature := try run(mem.c_allocator, &module, &store) + defer deinit(&premature) try expect_trap(premature, &store, .read_before_initialization, span) first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1) @@ -206,9 +213,11 @@ premature_local_reads_and_global_cycles_report_instruction_spans test { try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } }) try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } }) module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 } - cycle :: try run(mem.c_allocator, &module, &store) + cycle := try run(mem.c_allocator, &module, &store) + defer deinit(&cycle) try expect_trap(cycle, &store, .initialization_cycle, cycle_span) - retry :: try run(mem.c_allocator, &module, &store) + retry := try run(mem.c_allocator, &module, &store) + defer deinit(&retry) try expect_trap(retry, &store, .initialization_cycle, cycle_span) } @@ -219,7 +228,8 @@ existing_trap_identity_entry_failures_and_unused_errors_survive test { defer diagnostics.deinit(&store) entry_id :: try diagnostics.add(&store, .missing_entry, diagnostics.Span{ start = 0, end = 0 }) module.entry_diagnostic = entry_id - absent :: try run(mem.c_allocator, &module, &store) + absent := try run(mem.c_allocator, &module, &store) + defer deinit(&absent) try testing.expect_equal(entry_id, absent.trap?) invalid_id :: try diagnostics.add(&store, .unknown_name, diagnostics.Span{ start = 12, end = 17 }) bad :: try add_function(&module, mem.empty(ir.Type), 0, .invalid, 0) @@ -228,12 +238,14 @@ existing_trap_identity_entry_failures_and_unused_errors_survive test { module.entry = main try emit(&module, main, integer(0, 42)) return_value(&module, main, ir.ValueId(0)) - success :: try run(mem.c_allocator, &module, &store) + success := try run(mem.c_allocator, &module, &store) + defer deinit(&success) try testing.expect(success.trap == null) try testing.expect_equal(i64(42), success.value.data.integer) module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = function_value(0, bad) try emit(&module, main, ir.Instruction{ op = .call, type = .invalid, destination = ir.ValueId(1), left = ir.ValueId(0) }) - trapped :: try run(mem.c_allocator, &module, &store) + trapped := try run(mem.c_allocator, &module, &store) + defer deinit(&trapped) try testing.expect_equal(invalid_id, trapped.trap?) try testing.expect_equal(usize(2), diagnostics.count(&store)) } @@ -280,7 +292,8 @@ integer_arithmetic_checks_boundaries_before_host_operations test { try emit(&module, main, integer(1, case.right)) try emit(&module, main, ir.Instruction{ op = case.op, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1), span = span }) return_value(&module, main, ir.ValueId(2)) - outcome :: try run(mem.c_allocator, &module, &store) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) if case.expected |expected| { try testing.expect(outcome.trap == null) try testing.expect_equal(expected, outcome.value.data.integer) @@ -301,10 +314,12 @@ integer_negation_traps_only_at_minimum test { try emit(&module, main, integer(0, minval!(i64))) try emit(&module, main, ir.Instruction{ op = .negate, type = .int, destination = ir.ValueId(1), left = ir.ValueId(0), span = span }) return_value(&module, main, ir.ValueId(1)) - trapped :: try run(mem.c_allocator, &module, &store) + trapped := try run(mem.c_allocator, &module, &store) + defer deinit(&trapped) try expect_trap(trapped, &store, .integer_overflow, span) module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = integer(0, maxval!(i64)) - success :: try run(mem.c_allocator, &module, &store) + success := try run(mem.c_allocator, &module, &store) + defer deinit(&success) try testing.expect(success.trap == null) try testing.expect_equal(-maxval!(i64), success.value.data.integer) } @@ -324,18 +339,21 @@ float_arithmetic_and_ieee_zero_division_execute test { try emit(&module, main, ir.Instruction{ op = .divide, type = .float, destination = ir.ValueId(5), left = ir.ValueId(4), right = ir.ValueId(1) }) try emit(&module, main, ir.Instruction{ op = .negate, type = .float, destination = ir.ValueId(6), left = ir.ValueId(5) }) return_value(&module, main, ir.ValueId(6)) - result :: try run(mem.c_allocator, &module, &store) + result := try run(mem.c_allocator, &module, &store) + defer deinit(&result) try testing.expect(result.trap == null) try testing.expect_equal(ir.Type.float, result.value.type) try testing.expect_equal(f64(-10.5), result.value.data.real) module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 0 module.functions.items[usize(main)].blocks.items[0].instructions.items[1].constant.data.real = 0 - nan :: try run(mem.c_allocator, &module, &store) + nan := try run(mem.c_allocator, &module, &store) + defer deinit(&nan) try testing.expect(nan.trap == null) try testing.expect(nan.value.data.real != nan.value.data.real) module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 1 module.functions.items[usize(main)].blocks.items[0].instructions.items[2].op = .add - infinite :: try run(mem.c_allocator, &module, &store) + infinite := try run(mem.c_allocator, &module, &store) + defer deinit(&infinite) try testing.expect(infinite.trap == null) try testing.expect(infinite.value.data.real < f64(-1000000000)) try testing.expect_equal(infinite.value.data.real, infinite.value.data.real * f64(2)) @@ -403,7 +421,7 @@ recursive_invocations_use_heap_frames_and_free_on_allocation_failure test { try emit_at(&module, main, recursive_block, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) }) state := AllocationState{ remaining = 2048 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - _ = run(allocator, &module, &store) catch |error| { + unexpected := run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed @@ -412,6 +430,7 @@ recursive_invocations_use_heap_frames_and_free_on_allocation_failure test { try testing.expect_equal(usize(0), diagnostics.count(&store)) return } + deinit(&unexpected) return .expectation_failed } @@ -438,7 +457,7 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test { for (usize(0)..usize(16)) |limit| { state := AllocationState{ remaining = limit } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - outcome :: run(allocator, &module, &store) catch |error| { + outcome := run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed @@ -447,9 +466,11 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test { try testing.expect_equal(usize(0), state.live) continue } + defer deinit(&outcome) succeeded = true try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) + deinit(&outcome) try testing.expect_equal(usize(0), state.live) } try testing.expect(succeeded) @@ -458,8 +479,10 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test { module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id } state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - trapped :: try run(allocator, &module, &store) + trapped := try run(allocator, &module, &store) + defer deinit(&trapped) try testing.expect_equal(trap_id, trapped.trap?) + deinit(&trapped) try testing.expect_equal(usize(0), state.live) } @@ -477,7 +500,7 @@ runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames t defer diagnostics.deinit(&store) state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - _ = run(allocator, &module, &store) catch |error| { + unexpected := run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed @@ -487,6 +510,7 @@ runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames t try testing.expect_equal(usize(0), diagnostics.count(&store)) return } + deinit(&unexpected) return .expectation_failed } @@ -500,6 +524,7 @@ add_block proc(module @mut ir.Module, function ir.FunctionId) ir.BlockId ! mem.A @hide:file emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! mem.AllocError { + if (instruction.type == .address) module.functions.items[usize(function)].has_addresses = true try arraylist.append(&module.functions.items[usize(function)].blocks.items[usize(block)].instructions, instruction) } @@ -557,15 +582,19 @@ branching_recursive_calls_and_lazy_initializers_resume_the_current_block test { module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ value = ir.ValueId(4) } state := AllocationState{ remaining = 2048 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - outcome :: try run(allocator, &module, &store) + outcome := try run(allocator, &module, &store) + defer deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(130), outcome.value.data.integer) + deinit(&outcome) try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(1), diagnostics.count(&store)) # trapping after returning through many heap frames also releases every live frame. module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id } - trapped :: try run(allocator, &module, &store) + trapped := try run(allocator, &module, &store) + defer deinit(&trapped) try testing.expect_equal(trap_id, trapped.trap?) + deinit(&trapped) try testing.expect_equal(usize(0), state.live) } @@ -603,7 +632,8 @@ all_numeric_comparisons_dispatch_on_operand_type_not_bool_result_type test { .greater_equal: left >= right else: unreachable } - outcome :: try run(mem.c_allocator, &module, &store) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(expected, outcome.value.data.boolean) @@ -631,7 +661,8 @@ nan_comparisons_obey_ieee_rules_in_either_operand_position test { right ir.ValueId :: if (position == 0) ir.ValueId(0) else ir.ValueId(1) try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = left, right = right }) return_value(&module, main, ir.ValueId(2)) - outcome :: try run(mem.c_allocator, &module, &store) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(op == .not_equal, outcome.value.data.boolean) @@ -664,7 +695,8 @@ boolean_equality_and_branching_use_boolean_payloads test { equal :: left == right expected_true :: if (op == .equal) equal else !equal expected i64 :: if (expected_true) 42 else 7 - outcome :: try run(mem.c_allocator, &module, &store) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(expected, outcome.value.data.integer) } @@ -686,7 +718,8 @@ assign_local_replaces_initialized_values_without_producing_a_result test { try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(1) }) try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(2), slot = 0 }) return_value(&module, main, ir.ValueId(2)) - outcome :: try run(mem.c_allocator, &module, &store) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(i64(42), outcome.value.data.integer) @@ -712,7 +745,8 @@ reset_local_invalidates_reads_and_assignments_at_the_instruction_span test { try emit(&module, main, ir.Instruction{ op = op, type = .int, destination = destination, slot = 0, left = ir.ValueId(0), span = span, }) - outcome :: try run(mem.c_allocator, &module, &store) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) try expect_trap(outcome, &store, codes[index], span) try testing.expect_equal(usize(1), diagnostics.count(&store)) } @@ -736,7 +770,7 @@ premature_assignment_and_reset_traps_release_frames_at_all_allocation_boundaries for (usize(0)..usize(16)) |limit| { state := AllocationState{ remaining = limit } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - outcome :: run(allocator, &module, &store) catch |error| { + outcome := run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed @@ -745,8 +779,10 @@ premature_assignment_and_reset_traps_release_frames_at_all_allocation_boundaries try testing.expect_equal(usize(0), state.live) continue } + defer deinit(&outcome) succeeded = true try expect_trap(outcome, &store, .write_before_initialization, span) + deinit(&outcome) try testing.expect_equal(usize(0), state.live) } try testing.expect(succeeded) @@ -767,7 +803,7 @@ assignment_diagnostic_allocation_failure_releases_runtime_storage test { defer diagnostics.deinit(&store) state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - _ = run(allocator, &module, &store) catch |error| { + unexpected := run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed @@ -777,6 +813,7 @@ assignment_diagnostic_allocation_failure_releases_runtime_storage test { try testing.expect_equal(usize(0), diagnostics.count(&store)) return } + deinit(&unexpected) return .expectation_failed } @@ -799,14 +836,630 @@ logical_not_inverts_boolean_payload_and_composes_in_typed_ir test { try emit(&module, main, ir.Instruction{ op = .logical_not, type = .bool, destination = ir.ValueId(1), left = ir.ValueId(0) }) try emit(&module, main, ir.Instruction{ op = .logical_not, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(1) }) return_value(&module, main, ir.ValueId(1)) - inverted :: try run(mem.c_allocator, &module, &store) + inverted := try run(mem.c_allocator, &module, &store) + defer deinit(&inverted) try testing.expect(inverted.trap == null) try testing.expect_equal(ir.Type.bool, inverted.value.type) try testing.expect_equal(expected[index], inverted.value.data.boolean) return_value(&module, main, ir.ValueId(2)) - composed :: try run(mem.c_allocator, &module, &store) + composed := try run(mem.c_allocator, &module, &store) + defer deinit(&composed) try testing.expect(composed.trap == null) try testing.expect_equal(operand, composed.value.data.boolean) try testing.expect_equal(usize(0), diagnostics.count(&store)) } } + +@hide:file +unsigned proc(destination u32, value u64) ir.Instruction { + return ir.Instruction{ + op = .constant, type = .usize, destination = ir.ValueId(destination), + constant = ir.Value{ type = .usize, data = ir.ValueData{ unsigned = value } }, + } +} + +@hide:file +add_array proc(module @mut ir.Module, element ir.Type, child ir.ArrayId, len usize) ir.ArrayId ! mem.AllocError { + width :: if (element == .array) module.arrays.items[usize(child)].cells else 1 + id :: ir.ArrayId(module.arrays.items.len) + try arraylist.append(&module.arrays, ir.ArrayType{ + element = element, element_array = child, len = len, cells = len * width, mutable = true, + }) + return id +} + +@hide:file +array_storage proc(module @mut ir.Module, function ir.FunctionId, index u32, shape ir.ArrayId, local bool) void ! mem.AllocError { + body :: &module.functions.items[usize(function)] + try arraylist.append(&body.array_storage, ir.ArrayStorage{ + index = index, array_type = shape, offset = body.aggregate_cells, local = local, + }) + body.aggregate_cells += module.arrays.items[usize(shape)].cells +} + +@hide:file +literal proc(module @mut ir.Module, function ir.FunctionId, destination u32, shape ir.ArrayId, elements []u32) void ! mem.AllocError { + try array_storage(module, function, destination, shape, false) + block :: &module.functions.items[usize(function)].blocks.items[0] + start :: block.arguments.items.len + for (elements) |element| try arraylist.append(&block.arguments, ir.ValueId(element)) + try emit(module, function, ir.Instruction{ + op = .array_literal, type = .array, array_type = shape, destination = ir.ValueId(destination), + arguments_start = start, arguments_count = elements.len, + }) +} + +nested_array_places_and_rvalues_copy_only_their_typed_leaf_segments test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + row :: try add_array(&module, .int, ir.no_array, 2) + matrix :: try add_array(&module, .array, row, 2) + types [2]ir.Type :: [.array, .array] + main :: try add_function(&module, types[..], 0, .array, 15) + module.entry = main + module.functions.items[usize(main)].result_array_type = matrix + try array_storage(&module, main, 0, matrix, true) + try array_storage(&module, main, 1, matrix, true) + try emit(&module, main, integer(0, 1)) + try emit(&module, main, integer(1, 2)) + pair [2]u32 :: [0, 1] + try literal(&module, main, 2, row, pair[..]) + rows [2]u32 :: [2, 2] + try literal(&module, main, 3, matrix, rows[..]) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(3) }) + try array_storage(&module, main, 4, matrix, false) + try emit(&module, main, ir.Instruction{ op = .load_local, type = .array, array_type = matrix, destination = ir.ValueId(4), slot = 0 }) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 1, left = ir.ValueId(4) }) + try emit(&module, main, unsigned(5, 0)) + try emit(&module, main, unsigned(6, 1)) + try emit(&module, main, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = matrix, destination = ir.ValueId(7), slot = 0 }) + try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .array, array_type = row, destination = ir.ValueId(8), left = ir.ValueId(7), right = ir.ValueId(5) }) + try array_storage(&module, main, 9, row, false) + try emit(&module, main, ir.Instruction{ op = .load_address, type = .array, array_type = row, destination = ir.ValueId(9), left = ir.ValueId(8) }) + try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(10), left = ir.ValueId(8), right = ir.ValueId(6) }) + try emit(&module, main, integer(11, 9)) + try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(10), right = ir.ValueId(11) }) + # a row load is a private copy, not a view of the subsequently mutated matrix. + try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(8), right = ir.ValueId(9) }) + try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(10), right = ir.ValueId(11) }) + try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .array, array_type = row, destination = ir.ValueId(8), left = ir.ValueId(7), right = ir.ValueId(6) }) + try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(8), right = ir.ValueId(9) }) + try array_storage(&module, main, 12, matrix, false) + try emit(&module, main, ir.Instruction{ op = .load_local, type = .array, array_type = matrix, destination = ir.ValueId(12), slot = 0 }) + try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 1, left = ir.ValueId(12) }) + try emit(&module, main, integer(13, 7)) + try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(10), right = ir.ValueId(13) }) + try array_storage(&module, main, 14, matrix, false) + try emit(&module, main, ir.Instruction{ op = .load_local, type = .array, array_type = matrix, destination = ir.ValueId(14), slot = 1 }) + return_value(&module, main, ir.ValueId(14)) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(matrix, outcome.value.array_type) + expected [4]i64 :: [1, 9, 1, 2] + for (expected) |value, i| try testing.expect_equal(value, outcome.value.data.elements[i].data.integer) +} + +array_arguments_and_returns_are_private_values_before_frame_release test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + shape :: try add_array(&module, .int, ir.no_array, 1) + pair_shape :: try add_array(&module, .int, ir.no_array, 2) + types [2]ir.Type :: [.array, .array] + callee :: try add_function(&module, types[..], 1, .array, 6) + module.functions.items[usize(callee)].result_array_type = shape + try array_storage(&module, callee, 0, shape, true) + try array_storage(&module, callee, 1, shape, true) + try array_storage(&module, callee, 0, shape, false) + try array_storage(&module, callee, 5, shape, false) + try emit(&module, callee, ir.Instruction{ op = .load_local, type = .array, array_type = shape, destination = ir.ValueId(0), slot = 0 }) + try emit(&module, callee, ir.Instruction{ op = .store_local, left = ir.ValueId(0), slot = 1 }) + try emit(&module, callee, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(1), slot = 1 }) + try emit(&module, callee, unsigned(2, 0)) + try emit(&module, callee, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(3), left = ir.ValueId(1), right = ir.ValueId(2) }) + try emit(&module, callee, integer(4, 99)) + try emit(&module, callee, ir.Instruction{ op = .store_address, left = ir.ValueId(3), right = ir.ValueId(4) }) + try emit(&module, callee, ir.Instruction{ op = .load_local, type = .array, array_type = shape, destination = ir.ValueId(5), slot = 1 }) + # typed IR can address a parameter; even this mutation must not escape into the caller. + try emit(&module, callee, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(1), slot = 0 }) + try emit(&module, callee, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(3), left = ir.ValueId(1), right = ir.ValueId(2) }) + try emit(&module, callee, integer(4, 7)) + try emit(&module, callee, ir.Instruction{ op = .store_address, left = ir.ValueId(3), right = ir.ValueId(4) }) + return_value(&module, callee, ir.ValueId(5)) + main :: try add_function(&module, mem.empty(ir.Type), 0, .array, 8) + module.entry = main + module.functions.items[usize(main)].result_array_type = pair_shape + try emit(&module, main, integer(0, 42)) + element [1]u32 :: [0] + try literal(&module, main, 1, shape, element[..]) + try emit(&module, main, function_value(2, callee)) + try array_storage(&module, main, 3, shape, false) + try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(1)) + try emit(&module, main, ir.Instruction{ op = .call, type = .array, array_type = shape, destination = ir.ValueId(3), left = ir.ValueId(2), arguments_start = 1, arguments_count = 1 }) + try emit(&module, main, unsigned(4, 0)) + try emit(&module, main, ir.Instruction{ op = .index, type = .int, destination = ir.ValueId(5), left = ir.ValueId(1), right = ir.ValueId(4) }) + try emit(&module, main, ir.Instruction{ op = .index, type = .int, destination = ir.ValueId(6), left = ir.ValueId(3), right = ir.ValueId(4) }) + pair [2]u32 :: [5, 6] + try literal(&module, main, 7, pair_shape, pair[..]) + return_value(&module, main, ir.ValueId(7)) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.elements[0].data.integer) + try testing.expect_equal(i64(99), outcome.value.data.elements[1].data.integer) +} + +lazy_aggregate_globals_escape_with_owned_results_and_release_at_every_boundary test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + shape :: try add_array(&module, .int, ir.no_array, 1) + first :: try add_function(&module, mem.empty(ir.Type), 0, .array, 1) + second :: try add_function(&module, mem.empty(ir.Type), 0, .array, 2) + module.functions.items[usize(first)].result_array_type = shape + module.functions.items[usize(second)].result_array_type = shape + try array_storage(&module, first, 0, shape, false) + try emit(&module, first, ir.Instruction{ op = .load_global, type = .array, array_type = shape, destination = ir.ValueId(0), slot = 1 }) + return_value(&module, first, ir.ValueId(0)) + try emit(&module, second, integer(0, 42)) + operand [1]u32 :: [0] + try literal(&module, second, 1, shape, operand[..]) + return_value(&module, second, ir.ValueId(1)) + initializers [2]ir.FunctionId :: [first, second] + for (initializers) |initializer| { + try arraylist.append(&module.globals, ir.Global{ + type = .array, array_type = shape, initializer = initializer, + span = diagnostics.Span{ start = 0, end = 0 }, + }) + } + main :: try add_function(&module, mem.empty(ir.Type), 0, .array, 2) + module.entry = main + module.functions.items[usize(main)].result_array_type = shape + try emit(&module, main, ir.Instruction{ op = .address_global, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(0), slot = 0 }) + try array_storage(&module, main, 1, shape, false) + try emit(&module, main, ir.Instruction{ op = .load_address, type = .array, array_type = shape, destination = ir.ValueId(1), left = ir.ValueId(0) }) + return_value(&module, main, ir.ValueId(1)) + succeeded := false + failures usize := 0 + for (usize(0)..usize(40)) |limit| { + state := AllocationState{ remaining = limit } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + outcome := run(allocator, &module, &store) catch |error| { + if (error != .out_of_memory) return .expectation_failed + failures += 1 + try testing.expect_equal(usize(0), state.live) + continue + } + defer deinit(&outcome) + succeeded = true + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.elements[0].data.integer) + try testing.expect_equal(usize(1), state.live) + deinit(&outcome) + try testing.expect_equal(ir.Type.void, outcome.value.type) + try testing.expect(outcome.owned_elements == null) + try testing.expect_equal(usize(0), state.live) + } + try testing.expect(succeeded) + try testing.expect(failures > 0) + # restarting a run clears global readiness, including address-triggered initialization cycles. + cycle_span :: diagnostics.Span{ start = 51, end = 59 } + module.functions.items[usize(second)].has_addresses = true + module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = ir.Instruction{ + op = .address_global, type = .address, pointee = .array, array_type = shape, + destination = ir.ValueId(0), slot = 0, span = cycle_span, + } + state := AllocationState{ remaining = 64 } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + cycle := try run(allocator, &module, &store) + defer deinit(&cycle) + try expect_trap(cycle, &store, .initialization_cycle, cycle_span) + deinit(&cycle) + try testing.expect_equal(usize(0), state.live) + trap_id :: try diagnostics.add(&store, .invalid_escape, cycle_span) + module.functions.items[usize(second)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id } + module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = integer(0, 42) + trapped := try run(allocator, &module, &store) + defer deinit(&trapped) + try testing.expect_equal(trap_id, trapped.trap?) + deinit(&trapped) + try testing.expect_equal(usize(0), state.live) +} + +array_bounds_check_zero_lengths_and_the_full_unsigned_word_before_addressing test { + lengths [2]usize :: [0, 1] + indices [3]u64 :: [0, 1, maxval!(u64)] + operations [2]ir.Op :: [.index, .index_address] + for (lengths) |length| { + for (indices) |index| { + for (operations) |op| { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + shape :: try add_array(&module, .int, ir.no_array, length) + types [1]ir.Type :: [.array] + main :: try add_function(&module, types[..], 0, .int, 5) + module.entry = main + try array_storage(&module, main, 0, shape, true) + try emit(&module, main, integer(0, 42)) + operands [1]u32 :: [0] + try literal(&module, main, 1, shape, operands[..length]) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(1) }) + try emit(&module, main, unsigned(2, index)) + span :: diagnostics.Span{ start = 17, end = 29 } + base := ir.ValueId(1) + if (op == .index_address) { + try emit(&module, main, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(3), slot = 0 }) + base = ir.ValueId(3) + } + result_type ir.Type :: if (op == .index) .int else .address + try emit(&module, main, ir.Instruction{ + op = op, type = result_type, + pointee = .int, destination = ir.ValueId(4), left = base, right = ir.ValueId(2), span = span, + }) + if (op == .index_address) { + try emit(&module, main, ir.Instruction{ op = .load_address, type = .int, destination = ir.ValueId(0), left = ir.ValueId(4) }) + return_value(&module, main, ir.ValueId(0)) + } else return_value(&module, main, ir.ValueId(4)) + state := AllocationState{ remaining = 64 } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + outcome := try run(allocator, &module, &store) + defer deinit(&outcome) + if (length == 1 and index == 0) { + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.integer) + } else try expect_trap(outcome, &store, .index_out_of_bounds, span) + deinit(&outcome) + try testing.expect_equal(usize(0), state.live) + } + } + } +} + +zero_leaf_nested_arrays_return_a_shape_without_an_owned_allocation test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + empty :: try add_array(&module, .bool, ir.no_array, 0) + rows :: try add_array(&module, .array, empty, 3) + main :: try add_function(&module, mem.empty(ir.Type), 0, .array, 4) + module.entry = main + module.functions.items[usize(main)].result_array_type = rows + try literal(&module, main, 0, empty, mem.empty(u32)) + operands [3]u32 :: [0, 0, 0] + try literal(&module, main, 1, rows, operands[..]) + return_value(&module, main, ir.ValueId(1)) + state := AllocationState{ remaining = 64 } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + outcome := try run(allocator, &module, &store) + defer deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(rows, outcome.value.array_type) + try testing.expect_equal(usize(0), outcome.value.data.elements.len) + try testing.expect_equal(usize(0), state.live) + deinit(&outcome) + deinit(&outcome) + try testing.expect_equal(usize(0), state.live) + try emit(&module, main, unsigned(2, 2)) + try array_storage(&module, main, 3, empty, false) + try emit(&module, main, ir.Instruction{ op = .index, type = .array, array_type = empty, destination = ir.ValueId(3), left = ir.ValueId(1), right = ir.ValueId(2) }) + return_value(&module, main, ir.ValueId(3)) + module.functions.items[usize(main)].result_array_type = empty + indexed := try run(allocator, &module, &store) + defer deinit(&indexed) + try testing.expect(indexed.trap == null) + try testing.expect_equal(empty, indexed.value.array_type) + try testing.expect_equal(usize(0), indexed.value.data.elements.len) + deinit(&indexed) + try testing.expect_equal(usize(0), state.live) +} + +@hide:file +UnsignedCase :: struct { + op ir.Op + left u64 + right u64 + expected ?u64 +} + +unsigned_arithmetic_and_comparisons_preserve_all_sixty_four_bits test { + wide_sum u64 :: 9223372036854775815 + almost_maximum u64 :: 18446744073709551614 + cases [10]UnsignedCase :: [ + UnsignedCase{ op = .add, left = maxval!(u64), right = 0, expected = maxval!(u64) }, + UnsignedCase{ op = .add, left = maxval!(u64), right = 1, expected = null }, + UnsignedCase{ op = .add, left = 9223372036854775808, right = 7, expected = wide_sum }, + UnsignedCase{ op = .subtract, left = 0, right = 1, expected = null }, + UnsignedCase{ op = .subtract, left = maxval!(u64), right = 1, expected = almost_maximum }, + UnsignedCase{ op = .multiply, left = maxval!(u64), right = 1, expected = maxval!(u64) }, + UnsignedCase{ op = .multiply, left = 9223372036854775808, right = 2, expected = null }, + UnsignedCase{ op = .multiply, left = maxval!(u64), right = 0, expected = u64(0) }, + UnsignedCase{ op = .multiply, left = 0, right = maxval!(u64), expected = u64(0) }, + UnsignedCase{ op = .multiply, left = 3, right = 14, expected = u64(42) }, + ] + for (cases) |case| { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + main :: try add_function(&module, mem.empty(ir.Type), 0, .usize, 3) + module.entry = main + span :: diagnostics.Span{ start = 7, end = 12 } + try emit(&module, main, unsigned(0, case.left)) + try emit(&module, main, unsigned(1, case.right)) + try emit(&module, main, ir.Instruction{ op = case.op, type = .usize, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1), span = span }) + return_value(&module, main, ir.ValueId(2)) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) + if case.expected |expected| { + try testing.expect(outcome.trap == null) + try testing.expect_equal(ir.Type.usize, outcome.value.type) + try testing.expect_equal(expected, outcome.value.data.unsigned) + } else try expect_trap(outcome, &store, .integer_overflow, span) + } + operations [6]ir.Op :: [.equal, .not_equal, .less, .less_equal, .greater, .greater_equal] + expected [6]bool :: [false, true, false, false, true, true] + for (operations) |op, i| { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3) + module.entry = main + try emit(&module, main, unsigned(0, maxval!(u64))) + try emit(&module, main, unsigned(1, 1)) + try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) }) + return_value(&module, main, ir.ValueId(2)) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(expected[i], outcome.value.data.boolean) + } +} + +all_scalar_leaf_families_survive_aggregate_return_and_function_extraction test { + values [5]ir.Value :: [ + ir.Value{ type = .bool, data = ir.ValueData{ boolean = true } }, + ir.Value{ type = .float, data = ir.ValueData{ real = 3.25 } }, + ir.Value{ type = .string, data = ir.ValueData{ string = ir.StringId(0) } }, + ir.Value{ type = .usize, data = ir.ValueData{ unsigned = maxval!(u64) } }, + ir.Value{ type = .function, data = ir.ValueData{ function = ir.FunctionId(0) } }, + ] + for (values) |scalar| { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + if (scalar.type == .string) { + text :: "borrowed\x00text" + owned :: try mem.alloc(u8, module.allocator, text.len) + errdefer mem.free(module.allocator, owned) + memcopy!(owned, text) + try arraylist.append(&module.strings, ir.StringStorage{ allocation = owned, len = text.len }) + } + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + target :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1) + try emit(&module, target, integer(0, 42)) + return_value(&module, target, ir.ValueId(0)) + shape :: try add_array(&module, scalar.type, ir.no_array, 1) + main :: try add_function(&module, mem.empty(ir.Type), 0, .array, 5) + module.entry = main + module.functions.items[usize(main)].result_array_type = shape + try emit(&module, main, ir.Instruction{ op = .constant, type = scalar.type, destination = ir.ValueId(0), constant = scalar }) + operand [1]u32 :: [0] + try literal(&module, main, 1, shape, operand[..]) + return_value(&module, main, ir.ValueId(1)) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) + try testing.expect(outcome.trap == null) + leaf :: outcome.value.data.elements[0] + try testing.expect_equal(scalar.type, leaf.type) + match scalar.type { + .bool: try testing.expect(leaf.data.boolean) + .float: try testing.expect_equal(f64(3.25), leaf.data.real) + .string: try testing.expect_equal([]u8, "borrowed\x00text", ir.string_bytes(&module, leaf.data.string)) + .usize: try testing.expect_equal(maxval!(u64), leaf.data.unsigned) + .function: { + try testing.expect_equal(target, leaf.data.function) + try emit(&module, main, unsigned(2, 0)) + try emit(&module, main, ir.Instruction{ op = .index, type = .function, destination = ir.ValueId(3), left = ir.ValueId(1), right = ir.ValueId(2) }) + try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(4), left = ir.ValueId(3) }) + return_value(&module, main, ir.ValueId(4)) + called := try run(mem.c_allocator, &module, &store) + defer deinit(&called) + try testing.expect(called.trap == null) + try testing.expect_equal(i64(42), called.value.data.integer) + } + else: unreachable + } + } +} + +array_address_initialization_flags_survive_reset_and_diagnostic_allocation_failure test { + operations [2]ir.Op :: [.load_address, .store_address] + codes [2]diagnostics.Code :: [.read_before_initialization, .write_before_initialization] + for (operations) |op, i| { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + shape :: try add_array(&module, .int, ir.no_array, 1) + types [1]ir.Type :: [.array] + main :: try add_function(&module, types[..], 0, .int, 5) + module.entry = main + try array_storage(&module, main, 0, shape, true) + try emit(&module, main, integer(0, 42)) + operand [1]u32 :: [0] + try literal(&module, main, 1, shape, operand[..]) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(1) }) + try emit(&module, main, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(2), slot = 0 }) + try emit(&module, main, unsigned(3, 0)) + try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(4), left = ir.ValueId(2), right = ir.ValueId(3) }) + try emit(&module, main, ir.Instruction{ op = .reset_local, slot = 0 }) + span :: diagnostics.Span{ start = 23, end = 37 } + try emit(&module, main, ir.Instruction{ op = op, type = .int, destination = ir.ValueId(0), left = ir.ValueId(4), right = ir.ValueId(0), span = span }) + for (usize(0)..usize(2)) |attempt| { + state := AllocationState{ remaining = 64 } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + remaining usize :: if (attempt == 0) 0 else 64 + diagnostic_state := AllocationState{ remaining = remaining } + diagnostic_allocator :: mem.Allocator{ context = &diagnostic_state, vtable = &limited_vtable } + store := diagnostics.init(diagnostic_allocator) + defer diagnostics.deinit(&store) + outcome := run(allocator, &module, &store) catch |error| { + if (error != .out_of_memory) return .expectation_failed + try testing.expect_equal(usize(0), attempt) + try testing.expect_equal(usize(0), state.live) + try testing.expect_equal(usize(0), diagnostic_state.live) + continue + } + defer deinit(&outcome) + try testing.expect_equal(usize(1), attempt) + try expect_trap(outcome, &store, codes[i], span) + deinit(&outcome) + try testing.expect_equal(usize(0), state.live) + } + } +} + +aggregate_loop_places_reuse_storage_without_allocation_per_iteration test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + shape :: try add_array(&module, .int, ir.no_array, 1) + types [2]ir.Type :: [.array, .usize] + main :: try add_function(&module, types[..], 0, .int, 14) + module.entry = main + try array_storage(&module, main, 0, shape, true) + try emit(&module, main, integer(0, 0)) + operand [1]u32 :: [0] + try literal(&module, main, 1, shape, operand[..]) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(1) }) + try emit(&module, main, unsigned(2, 0)) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 1, left = ir.ValueId(2) }) + try emit(&module, main, unsigned(3, 1)) + header :: try add_block(&module, main) + body :: try add_block(&module, main) + done :: try add_block(&module, main) + module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = header } + try emit_at(&module, main, header, ir.Instruction{ op = .load_local, type = .usize, destination = ir.ValueId(4), slot = 1 }) + try emit_at(&module, main, header, ir.Instruction{ op = .less, type = .bool, destination = ir.ValueId(5), left = ir.ValueId(4), right = ir.ValueId(3) }) + module.functions.items[usize(main)].blocks.items[usize(header)].terminator = ir.Terminator{ kind = .branch, value = ir.ValueId(5), target = body, alternative = done } + try emit_at(&module, main, body, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(6), slot = 0 }) + try emit_at(&module, main, body, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(7), left = ir.ValueId(6), right = ir.ValueId(2) }) + try emit_at(&module, main, body, ir.Instruction{ op = .load_address, type = .int, destination = ir.ValueId(8), left = ir.ValueId(7) }) + try emit_at(&module, main, body, integer(9, 1)) + try emit_at(&module, main, body, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(10), left = ir.ValueId(8), right = ir.ValueId(9) }) + try emit_at(&module, main, body, ir.Instruction{ op = .store_address, left = ir.ValueId(7), right = ir.ValueId(10) }) + try arraylist.append(&module.functions.items[usize(main)].blocks.items[usize(body)].arguments, ir.ValueId(10)) + try emit_at(&module, main, body, ir.Instruction{ op = .array_literal, type = .array, array_type = shape, destination = ir.ValueId(1), arguments_count = 1 }) + try emit_at(&module, main, body, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(1) }) + try emit_at(&module, main, body, unsigned(11, 1)) + try emit_at(&module, main, body, ir.Instruction{ op = .add, type = .usize, destination = ir.ValueId(12), left = ir.ValueId(4), right = ir.ValueId(11) }) + try emit_at(&module, main, body, ir.Instruction{ op = .assign_local, slot = 1, left = ir.ValueId(12) }) + module.functions.items[usize(main)].blocks.items[usize(body)].terminator = ir.Terminator{ kind = .jump, target = header } + try emit_at(&module, main, done, ir.Instruction{ op = .load_address, type = .int, destination = ir.ValueId(13), left = ir.ValueId(7) }) + module.functions.items[usize(main)].blocks.items[usize(done)].terminator = ir.Terminator{ value = ir.ValueId(13) } + iterations [2]u64 :: [1, 10000] + allocations usize := 0 + for (iterations) |count, i| { + module.functions.items[usize(main)].blocks.items[0].instructions.items[5] = unsigned(3, count) + state := AllocationState{ remaining = 64 } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + outcome := try run(allocator, &module, &store) + defer deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(count), outcome.value.data.integer) + deinit(&outcome) + try testing.expect_equal(usize(0), state.live) + consumed :: usize(64) - state.remaining + if (i == 0) allocations = consumed + else try testing.expect_equal(allocations, consumed) + } +} + +recursive_array_backings_and_addresses_remain_fresh_across_stack_growth test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + shape :: try add_array(&module, .int, ir.no_array, 1) + types [2]ir.Type :: [.int, .array] + recursive :: try add_function(&module, types[..], 1, .int, 12) + try array_storage(&module, recursive, 1, shape, true) + try emit(&module, recursive, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 }) + operand [1]u32 :: [0] + try literal(&module, recursive, 1, shape, operand[..]) + try emit(&module, recursive, ir.Instruction{ op = .store_local, slot = 1, left = ir.ValueId(1) }) + try emit(&module, recursive, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(2), slot = 1 }) + try emit(&module, recursive, unsigned(3, 0)) + try emit(&module, recursive, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(4), left = ir.ValueId(2), right = ir.ValueId(3) }) + try emit(&module, recursive, integer(5, 0)) + try emit(&module, recursive, ir.Instruction{ op = .equal, type = .bool, destination = ir.ValueId(6), left = ir.ValueId(0), right = ir.ValueId(5) }) + done :: try add_block(&module, recursive) + recurse :: try add_block(&module, recursive) + module.functions.items[usize(recursive)].blocks.items[0].terminator = ir.Terminator{ kind = .branch, value = ir.ValueId(6), target = done, alternative = recurse } + module.functions.items[usize(recursive)].blocks.items[usize(done)].terminator = ir.Terminator{ value = ir.ValueId(5) } + try emit_at(&module, recursive, recurse, integer(5, 1)) + try emit_at(&module, recursive, recurse, ir.Instruction{ op = .subtract, type = .int, destination = ir.ValueId(7), left = ir.ValueId(0), right = ir.ValueId(5) }) + try emit_at(&module, recursive, recurse, function_value(8, recursive)) + try arraylist.append(&module.functions.items[usize(recursive)].blocks.items[usize(recurse)].arguments, ir.ValueId(7)) + try emit_at(&module, recursive, recurse, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(9), left = ir.ValueId(8), arguments_count = 1 }) + try emit_at(&module, recursive, recurse, ir.Instruction{ op = .load_address, type = .int, destination = ir.ValueId(10), left = ir.ValueId(4) }) + try emit_at(&module, recursive, recurse, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(11), left = ir.ValueId(9), right = ir.ValueId(10) }) + module.functions.items[usize(recursive)].blocks.items[usize(recurse)].terminator = ir.Terminator{ value = ir.ValueId(11) } + main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3) + module.entry = main + try emit(&module, main, function_value(0, recursive)) + try emit(&module, main, integer(1, 64)) + try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(1)) + try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), arguments_count = 1 }) + return_value(&module, main, ir.ValueId(2)) + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(2080), outcome.value.data.integer) +} + +inclusive_unsigned_range_reaches_the_maximum_without_incrementing_past_it test { + module := ir.init(mem.c_allocator) + defer ir.deinit(&module) + store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&store) + types [2]ir.Type :: [.usize, .usize] + main :: try add_function(&module, types[..], 0, .usize, 9) + module.entry = main + try emit(&module, main, unsigned(0, maxval!(u64) - 1)) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) }) + try emit(&module, main, unsigned(1, 0)) + try emit(&module, main, ir.Instruction{ op = .store_local, slot = 1, left = ir.ValueId(1) }) + try emit(&module, main, unsigned(2, maxval!(u64))) + try emit(&module, main, unsigned(3, 1)) + header :: try add_block(&module, main) + body :: try add_block(&module, main) + advance :: try add_block(&module, main) + done :: try add_block(&module, main) + module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = header } + try emit_at(&module, main, header, ir.Instruction{ op = .load_local, type = .usize, destination = ir.ValueId(4), slot = 0 }) + try emit_at(&module, main, header, ir.Instruction{ op = .less_equal, type = .bool, destination = ir.ValueId(5), left = ir.ValueId(4), right = ir.ValueId(2) }) + module.functions.items[usize(main)].blocks.items[usize(header)].terminator = ir.Terminator{ kind = .branch, value = ir.ValueId(5), target = body, alternative = done } + try emit_at(&module, main, body, ir.Instruction{ op = .load_local, type = .usize, destination = ir.ValueId(6), slot = 1 }) + try emit_at(&module, main, body, ir.Instruction{ op = .add, type = .usize, destination = ir.ValueId(7), left = ir.ValueId(6), right = ir.ValueId(3) }) + try emit_at(&module, main, body, ir.Instruction{ op = .assign_local, slot = 1, left = ir.ValueId(7) }) + try emit_at(&module, main, body, ir.Instruction{ op = .equal, type = .bool, destination = ir.ValueId(8), left = ir.ValueId(4), right = ir.ValueId(2) }) + module.functions.items[usize(main)].blocks.items[usize(body)].terminator = ir.Terminator{ kind = .branch, value = ir.ValueId(8), target = done, alternative = advance } + try emit_at(&module, main, advance, ir.Instruction{ op = .add, type = .usize, destination = ir.ValueId(0), left = ir.ValueId(4), right = ir.ValueId(3) }) + try emit_at(&module, main, advance, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(0) }) + module.functions.items[usize(main)].blocks.items[usize(advance)].terminator = ir.Terminator{ kind = .jump, target = header } + try emit_at(&module, main, done, ir.Instruction{ op = .load_local, type = .usize, destination = ir.ValueId(1), slot = 1 }) + module.functions.items[usize(main)].blocks.items[usize(done)].terminator = ir.Terminator{ value = ir.ValueId(1) } + outcome := try run(mem.c_allocator, &module, &store) + defer deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(u64(2), outcome.value.data.unsigned) +} diff --git a/source/ir/ir.hon b/source/ir/ir.hon index 0382d04..4be8a25 100644 --- a/source/ir/ir.hon +++ b/source/ir/ir.hon @@ -8,6 +8,8 @@ LocalId :: distinct u32 ValueId :: distinct u32 BlockId :: distinct u32 StringId :: distinct u32 +ArrayId :: distinct u32 +no_array :: maxval!(ArrayId) no_value :: maxval!(ValueId) Type :: enum { @@ -18,20 +20,44 @@ Type :: enum { string, void, function, + array, + usize, + address, } ValueData :: union { integer i64 + unsigned u64 real f64 boolean bool string StringId function FunctionId + elements []mut Value + address u32 } Value :: struct { type Type = .void + array_type ArrayId = no_array data ValueData = ValueData{ integer = 0 } } +#! fixed recursive shape; `cells` counts flattened scalar leaves, including zero-sized children. +ArrayType :: struct { + element Type + element_array ArrayId = no_array + len usize + cells usize + mutable bool +} + +#! maps a local slot or temporary to its preallocated, by-value frame segment. +ArrayStorage :: struct { + index u32 + array_type ArrayId + offset usize + local bool +} + Op :: enum { constant load_local @@ -39,6 +65,13 @@ Op :: enum { assign_local reset_local load_global + array_literal + index + address_local + address_global + index_address + load_address + store_address negate logical_not add @@ -57,6 +90,8 @@ Op :: enum { Instruction :: struct { op Op type Type = .void + array_type ArrayId = no_array + pointee Type = .invalid span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 } destination ValueId = no_value left ValueId = no_value @@ -93,6 +128,10 @@ Function :: struct { parameters arraylist.ArrayList(Type) locals arraylist.ArrayList(Type) result Type = .void + result_array_type ArrayId = no_array + array_storage arraylist.ArrayList(ArrayStorage) + aggregate_cells usize = 0 + has_addresses bool = false blocks arraylist.ArrayList(Block) entry BlockId = BlockId(0) value_count usize = 0 @@ -101,6 +140,7 @@ Function :: struct { Global :: struct { type Type + array_type ArrayId = no_array initializer FunctionId span diagnostics.Span } @@ -118,6 +158,7 @@ Module :: struct { functions arraylist.ArrayList(Function) globals arraylist.ArrayList(Global) strings arraylist.ArrayList(StringStorage) + arrays arraylist.ArrayList(ArrayType) entry ?FunctionId = null entry_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic } @@ -128,6 +169,7 @@ init proc(allocator mem.Allocator) Module { functions = arraylist.init(allocator), globals = arraylist.init(allocator), strings = arraylist.init(allocator), + arrays = arraylist.init(allocator), } } @@ -136,6 +178,7 @@ init_function proc(allocator mem.Allocator) Function { parameters = arraylist.init(allocator), locals = arraylist.init(allocator), blocks = arraylist.init(allocator), + array_storage = arraylist.init(allocator), } } @@ -164,6 +207,7 @@ deinit proc(module @mut Module) void { arraylist.deinit(&function.parameters) arraylist.deinit(&function.locals) arraylist.deinit(&function.blocks) + arraylist.deinit(&function.array_storage) } for (module.strings.items) |string| mem.free(module.allocator, string.allocation) @@ -171,4 +215,5 @@ deinit proc(module @mut Module) void { arraylist.deinit(&module.functions) arraylist.deinit(&module.globals) arraylist.deinit(&module.strings) + arraylist.deinit(&module.arrays) } diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index 5d96029..7646e75 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -32,6 +32,7 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([ { "false", .false }, { "and", .and }, { "or", .or }, + { "mut", .mut }, ]) #! owns token storage and retains its allocator; release with deinit. diff --git a/source/lexer/scan.test.hon b/source/lexer/scan.test.hon index 36556d0..d58bca2 100644 --- a/source/lexer/scan.test.hon +++ b/source/lexer/scan.test.hon @@ -122,3 +122,22 @@ scans_ranges_without_consuming_float_dots test { try testing.expect_equal(u32(10), state.tokens.items[4].span.end) try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) } + +scans_array_delimiters_and_mut_keyword 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) + state := init(mem.c_allocator) + defer deinit(&state) + try scan(&state, "[_]mut [2]i64 [1,2] [] mutable mut_1", &diagnostic_store) + expected [18]TokenKind :: [ + .open_bracket, .ident, .close_bracket, .mut, + .open_bracket, .int, .close_bracket, .ident, + .open_bracket, .int, .comma, .int, .close_bracket, + .open_bracket, .close_bracket, .ident, .ident, .eof, + ] + try testing.expect_equal(expected.len, state.tokens.items.len) + for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) +} diff --git a/source/lexer/token.hon b/source/lexer/token.hon index 62e7fc8..be503c1 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -24,6 +24,7 @@ TokenKind :: enum { break, continue true, false and, or + mut # literals ident, string diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 8da4bc6..71d0aee 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -30,6 +30,7 @@ State :: struct { diagnostics @mut diagnostics.Store module @mut ir.Module storage []mut Storage + arrays []mut ir.ArrayId arguments arraylist.ArrayList(ir.ValueId) function usize = 0 block ir.BlockId = ir.BlockId(0) @@ -51,6 +52,9 @@ lower proc( storage :: try mem.alloc(Storage, allocator, ast.node_count(tree)) defer mem.free(allocator, storage) mem.set(storage, Storage{}) + arrays :: try mem.alloc(ir.ArrayId, allocator, typecheck.array_type_count(checked)) + defer mem.free(allocator, arrays) + mem.set(arrays, ir.no_array) state := State{ tree = tree, resolution = resolution, @@ -58,6 +62,7 @@ lower proc( diagnostics = diagnostic_store, module = &module, storage = storage, + arrays = arrays, arguments = arraylist.init(ir.ValueId, allocator), } defer arraylist.deinit(&state.arguments) @@ -75,6 +80,7 @@ lower proc( global :: u32(module.globals.items.len) try arraylist.append(&module.globals, ir.Global{ type = lower_type(typecheck.type_of(checked, id)), + array_type = try array_shape(&state, typecheck.type_of(checked, id)), initializer = initializer, span = ast.node_span(tree, id), }) @@ -99,6 +105,8 @@ lower proc( lower_type proc(type typecheck.Type) ir.Type { return match type.kind { .int: .int + .usize: .usize + .array: .array .float: .float .bool: .bool .string: .string @@ -108,6 +116,53 @@ lower_type proc(type typecheck.Type) ir.Type { } } +@hide:file +array_shape proc(state @mut State, type typecheck.Type) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) { + if (type.kind != .array) return ir.no_array + # checker history includes unresolved shapes; materialize only final used types. + return try prepare_array(state, type.array_type) +} + +@hide:file +prepare_array proc(state @mut State, id typecheck.ArrayId) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) { + if (state.arrays[usize(id)] != ir.no_array) return state.arrays[usize(id)] + descriptor :: typecheck.array_type(state.checked, id) + child := ir.no_array + width := usize(1) + if (descriptor.element.kind == .array) { + child = try prepare_array(state, descriptor.element.array_type) + width = state.module.arrays.items[usize(child)].cells + } + if (width != 0 and descriptor.len > divtrunc!(maxval!(usize), width)) return .storage_limit_reached + if (descriptor.len * width > divtrunc!(maxval!(usize), sizeof!(ir.Value))) return .storage_limit_reached + if (state.module.arrays.items.len >= usize(ir.no_array)) return .storage_limit_reached + result :: ir.ArrayId(state.module.arrays.items.len) + try arraylist.append(&state.module.arrays, ir.ArrayType{ + element = lower_type(descriptor.element), + element_array = child, + len = descriptor.len, + cells = descriptor.len * width, + mutable = descriptor.mutable, + }) + state.arrays[usize(id)] = result + return result +} + +@hide:file +prepare_storage proc(state @mut State, index u32, shape ir.ArrayId, local bool) void ! (mem.AllocError | diagnostics.CapacityError) { + if (shape == ir.no_array) return + function := &state.module.functions.items[state.function] + cells :: state.module.arrays.items[usize(shape)].cells + if (cells > divtrunc!(maxval!(usize), sizeof!(ir.Value)) - function.aggregate_cells) return .storage_limit_reached + try arraylist.append(&function.array_storage, ir.ArrayStorage{ + index = index, + array_type = shape, + offset = function.aggregate_cells, + local = local, + }) + function.aggregate_cells += cells +} + @hide:file add_function proc(module @mut ir.Module, span diagnostics.Span) ir.FunctionId ! mem.AllocError { id :: ir.FunctionId(module.functions.items.len) @@ -182,19 +237,22 @@ finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void { } @hide:file -emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! mem.AllocError { +emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { function := &state.module.functions.items[state.function] operation := instruction - if (operation.op != .store_local and operation.op != .assign_local and operation.op != .reset_local) { + if (operation.op != .store_local and operation.op != .assign_local and operation.op != .reset_local and operation.op != .store_address) { + if (function.value_count >= usize(ir.no_value)) return .storage_limit_reached operation.destination = ir.ValueId(function.value_count) function.value_count += 1 + if (operation.type == .array) try prepare_storage(state, u32(operation.destination), operation.array_type, false) } + if (operation.type == .address) function.has_addresses = true try arraylist.append(&function.blocks.items[usize(state.block)].instructions, operation) return operation.destination } @hide:file -constant proc(state @mut State, value ir.Value, span diagnostics.Span) ir.ValueId ! mem.AllocError { +constant proc(state @mut State, value ir.Value, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { return try emit(state, ir.Instruction{ op = .constant, type = value.type, constant = value, span = span }) } @@ -205,6 +263,7 @@ lower_global proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diag state.block = ir.BlockId(0) state.terminated = false state.module.functions.items[state.function].result = global.type + state.module.functions.items[state.function].result_array_type = global.array_type if (trap_binding(state, id)) return declaration :: ast.statement_decl(state.tree, id) if (declaration.type) |annotation| if (trap_type(state, annotation)) return @@ -222,12 +281,14 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di state.terminated = false function := &state.module.functions.items[state.function] function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type)) + function.result_array_type = try array_shape(state, typecheck.type_of(state.checked, declaration.return_type)) for (0..ast.function_param_count(tree, id)) |i| { parameter :: ast.function_param(tree, id, i) type :: lower_type(typecheck.type_of(state.checked, parameter)) state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(function.locals.items.len) } try arraylist.append(&function.parameters, type) try arraylist.append(&function.locals, type) + try prepare_storage(state, state.storage[usize(parameter)].index, try array_shape(state, typecheck.type_of(state.checked, parameter)), true) } try predeclare_locals(state, declaration.body) # signature errors are entry traps; body errors are reached in statement order. @@ -252,7 +313,7 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di } @hide:file -predeclare_locals proc(state @mut State, block ast.NodeId) void ! mem.AllocError { +predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { for (0..ast.block_statement_count(state.tree, block)) |i| { statement :: ast.block_statement(state.tree, block, i) match ast.stmt_kind(state.tree, statement) { @@ -260,6 +321,7 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! mem.AllocError function := &state.module.functions.items[state.function] state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) } try arraylist.append(&function.locals, lower_type(typecheck.type_of(state.checked, statement))) + try prepare_storage(state, state.storage[usize(statement)].index, try array_shape(state, typecheck.type_of(state.checked, statement)), true) } .if_stmt: { conditional :: ast.if_stmt(state.tree, statement) @@ -271,7 +333,9 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! mem.AllocError loop :: ast.for_stmt(state.tree, statement) function := &state.module.functions.items[state.function] slot :: u32(function.locals.items.len) - try arraylist.append(&function.locals, ir.Type.int) + family :: typecheck.type_of(state.checked, loop.iterable).range_element + type ir.Type :: if (family == .usize) .usize else .int + try arraylist.append(&function.locals, type) state.storage[usize(statement)] = Storage{ kind = .local, index = slot } if (loop.capture) |capture| state.storage[usize(capture)] = state.storage[usize(statement)] if (loop.index) |index| { @@ -405,7 +469,8 @@ lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnos end :: try lower_expression(state, interval.end) if (state.terminated or trap_type(state, loop.iterable) or trap_type(state, id)) return slot :: state.storage[usize(id)].index - _ = try emit(state, ir.Instruction{ op = .store_local, type = .int, left = start, slot = slot, span = interval.span }) + family :: lower_type(typecheck.Type{ kind = typecheck.type_of(state.checked, loop.iterable).range_element }) + _ = try emit(state, ir.Instruction{ op = .store_local, type = family, left = start, slot = slot, span = interval.span }) origin :: state.block condition :: try add_block(state) body :: try add_block(state) @@ -416,7 +481,7 @@ lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnos kind = .jump, target = condition, span = loop.span, } state.block = condition - current :: try emit(state, ir.Instruction{ op = .load_local, type = .int, slot = slot, span = interval.span }) + current :: try emit(state, ir.Instruction{ op = .load_local, type = family, slot = slot, span = interval.span }) comparison ir.Op :: if (interval.inclusive) .less_equal else .less condition_value :: try emit(state, ir.Instruction{ op = comparison, type = .bool, left = current, right = end, span = interval.span }) state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ @@ -435,16 +500,18 @@ lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnos } state.block = update state.terminated = false - last :: try emit(state, ir.Instruction{ op = .load_local, type = .int, slot = slot, span = interval.span }) + last :: try emit(state, ir.Instruction{ op = .load_local, type = family, slot = slot, span = interval.span }) finished :: try emit(state, ir.Instruction{ op = .equal, type = .bool, left = last, right = end, span = interval.span }) # continue reaches this guard too, so an inclusive maximum never adds one. state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ kind = .branch, value = finished, target = exit, alternative = increment, span = interval.span, } state.block = increment - one :: try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } }, interval.span) - next :: try emit(state, ir.Instruction{ op = .add, type = .int, left = last, right = one, span = interval.span }) - _ = try emit(state, ir.Instruction{ op = .store_local, type = .int, left = next, slot = slot, span = interval.span }) + unit := ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } } + if (family == .usize) unit = ir.Value{ type = .usize, data = ir.ValueData{ unsigned = 1 } } + one :: try constant(state, unit, interval.span) + next :: try emit(state, ir.Instruction{ op = .add, type = family, left = last, right = one, span = interval.span }) + _ = try emit(state, ir.Instruction{ op = .store_local, type = family, left = next, slot = slot, span = interval.span }) state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ kind = .jump, target = condition, span = loop.span, } @@ -473,6 +540,25 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d } .assignment_stmt: { assignment :: ast.assignment_stmt(tree, id) + if (ast.expr_kind(tree, assignment.target) == .index) { + value :: try lower_expression(state, assignment.value) + if (state.terminated) return + if (!is_place(state, assignment.target)) { + _ = try lower_expression(state, assignment.target) + if (!state.terminated) _ = trap_type(state, id) + return + } + address :: try lower_place(state, assignment.target) + if (state.terminated or trap_type(state, id)) return + _ = try emit(state, ir.Instruction{ + op = .store_address, + type = .void, + left = address, + right = value, + span = ast.node_span(tree, assignment.target), + }) + return + } if (trap_binding(state, assignment.target) or trap_type(state, id)) return value :: try lower_expression(state, assignment.value) if (state.terminated) return @@ -480,6 +566,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d _ = try emit(state, ir.Instruction{ op = .assign_local, type = lower_type(typecheck.type_of(state.checked, assignment.target)), + array_type = try array_shape(state, typecheck.type_of(state.checked, assignment.target)), left = value, slot = state.storage[usize(binding.data.declaration)].index, span = ast.node_span(tree, assignment.target), @@ -499,6 +586,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d _ = try emit(state, ir.Instruction{ op = .store_local, type = lower_type(typecheck.type_of(state.checked, id)), + array_type = try array_shape(state, typecheck.type_of(state.checked, id)), left = value, slot = state.storage[usize(id)].index, span = declaration.span, @@ -556,11 +644,61 @@ lower_logical proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError return try emit(state, ir.Instruction{ op = .load_local, type = .bool, slot = slot, span = span }) } +@hide:file +is_place proc(state @State, id ast.NodeId) bool { + match ast.expr_kind(state.tree, id) { + .index: return is_place(state, ast.index_expr(state.tree, id).base) + .identifier: { + binding :: sema.binding(state.resolution, id) + if (binding.kind == .invalid) return true + if (binding.kind != .declaration) return false + location :: state.storage[usize(binding.data.declaration)] + return location.kind == .local or location.kind == .global + } + else: return false + } +} + +@hide:file +lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + span :: ast.node_span(state.tree, id) + checked :: typecheck.type_of(state.checked, id) + if (ast.expr_kind(state.tree, id) == .index) { + expression :: ast.index_expr(state.tree, id) + base :: try lower_place(state, expression.base) + if (state.terminated) return ir.no_value + index :: try lower_expression(state, expression.index) + if (state.terminated or trap_type(state, id)) return ir.no_value + return try emit(state, ir.Instruction{ + op = .index_address, + type = .address, + pointee = lower_type(checked), + array_type = try array_shape(state, checked), + left = base, + right = index, + span = span, + }) + } + if (trap_binding(state, id) or trap_type(state, id)) return ir.no_value + binding :: sema.binding(state.resolution, id) + location :: state.storage[usize(binding.data.declaration)] + operation ir.Op :: if (location.kind == .global) .address_global else .address_local + return try emit(state, ir.Instruction{ + op = operation, + type = .address, + pointee = lower_type(checked), + array_type = try array_shape(state, checked), + slot = location.index, + span = span, + }) +} + @hide:file lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { tree :: state.tree span :: ast.node_span(tree, id) type :: lower_type(typecheck.type_of(state.checked, id)) + shape :: try array_shape(state, typecheck.type_of(state.checked, id)) match ast.expr_kind(tree, id) { .invalid: { invalid :: ast.invalid(tree, id) @@ -570,6 +708,53 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr # ranges have no physical value; only lower_for decomposes an iterable. if (!trap_type(state, id)) unreachable } + .array_type: { + if (!trap_type(state, id)) { + trap(state, try diagnostics.add(state.diagnostics, .type_not_value, span), span) + } + } + .array_literal: { + scratch_start :: state.arguments.items.len + defer state.arguments.items = state.arguments.items.ptr[..scratch_start] + count :: ast.array_element_count(tree, id) + try arraylist.reserve(&state.arguments, scratch_start + count) + for (0..count) |_| try arraylist.append(&state.arguments, ir.no_value) + for (0..count) |i| { + element :: try lower_expression(state, ast.array_element(tree, id, i)) + if (state.terminated) return ir.no_value + state.arguments.items[scratch_start + i] = element + } + if (trap_type(state, id)) return ir.no_value + block := &state.module.functions.items[state.function].blocks.items[usize(state.block)] + start :: block.arguments.items.len + try arraylist.reserve(&block.arguments, start + count) + for (0..count) |i| try arraylist.append(&block.arguments, state.arguments.items[scratch_start + i]) + return try emit(state, ir.Instruction{ + op = .array_literal, + type = .array, + array_type = shape, + arguments_start = start, + arguments_count = count, + span = span, + }) + } + .index: { + if (is_place(state, id)) { + address :: try lower_place(state, id) + if (state.terminated) return ir.no_value + return try emit(state, ir.Instruction{ + op = .load_address, type = type, array_type = shape, left = address, span = span, + }) + } + expression :: ast.index_expr(tree, id) + base :: try lower_expression(state, expression.base) + if (state.terminated) return ir.no_value + index :: try lower_expression(state, expression.index) + if (state.terminated or trap_type(state, id)) return ir.no_value + return try emit(state, ir.Instruction{ + op = .index, type = type, array_type = shape, left = base, right = index, span = span, + }) + } .literal_int, .literal_float: { if (trap_type(state, id)) return ir.no_value return try lower_number(state, id) @@ -594,8 +779,8 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr type = .function, data = ir.ValueData{ function = ir.FunctionId(location.index) }, }, span) - .global: return try emit(state, ir.Instruction{ op = .load_global, type = type, slot = location.index, span = span }) - .local: return try emit(state, ir.Instruction{ op = .load_local, type = type, slot = location.index, span = span }) + .global: return try emit(state, ir.Instruction{ op = .load_global, type = type, array_type = shape, slot = location.index, span = span }) + .local: return try emit(state, ir.Instruction{ op = .load_local, type = type, array_type = shape, slot = location.index, span = span }) .none: unreachable } } @@ -664,6 +849,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr return try emit(state, ir.Instruction{ op = .call, type = type, + array_type = shape, left = callee, arguments_start = start, arguments_count = count, @@ -695,6 +881,10 @@ lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError if (integer_magnitude(text, u64(maxval!(i64)))) |magnitude| { return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = i64(magnitude) } }, span) } + } else if (type == .usize) { + if (integer_magnitude(text, maxval!(u64))) |magnitude| { + return try constant(state, ir.Value{ type = .usize, data = ir.ValueData{ unsigned = magnitude } }, span) + } } else { buffer :: try mem.alloc(u8, state.module.allocator, text.len + 1) defer mem.free(state.module.allocator, buffer) diff --git a/source/lower/lower.test.hon b/source/lower/lower.test.hon index 4a85937..63f4fde 100644 --- a/source/lower/lower.test.hon +++ b/source/lower/lower.test.hon @@ -68,7 +68,8 @@ executes_calls_locals_arithmetic_and_nested_argument_slices test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(i64(42), outcome.value.data.integer) @@ -88,7 +89,8 @@ owns_decoded_strings_after_source_and_frontend_are_freed test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.string, outcome.value.type) try testing.expect(mem.eql(ir.string_bytes(&module, outcome.value.data.string), "a\\\"\n\r\t\x00Az")) @@ -118,7 +120,8 @@ honors_float_context_and_float_division test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.float, outcome.value.type) try testing.expect_equal(f64(0.5), outcome.value.data.real) @@ -142,10 +145,12 @@ executes_forward_lazy_globals_and_function_aliases test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) - again :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + again := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&again) try testing.expect(again.trap == null) try testing.expect_equal(i64(42), again.value.data.integer) } @@ -162,7 +167,8 @@ supports_bare_void_value_returns_and_fallthrough test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.void, outcome.value.type) } @@ -185,7 +191,8 @@ preserves_demanded_diagnostic_identity_and_source_location test { diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? try testing.expect_equal(diagnostics.Code.unknown_name, diagnostic.code) try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "unknown")) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) } @@ -207,7 +214,8 @@ skips_unreachable_and_unused_invalid_code test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect(diagnostics.count(&diagnostic_store) > 0) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) } @@ -224,7 +232,8 @@ traps_local_forward_loads_and_global_initialization_cycles test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) code diagnostics.Code :: if (i == 0) .read_before_initialization else .initialization_cycle try expect_trap(outcome, &diagnostic_store, code) } @@ -243,7 +252,8 @@ handles_integer_boundaries_without_host_overflow test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(expected[i], outcome.value.data.integer) } @@ -265,7 +275,8 @@ traps_invalid_literals_escapes_and_checked_integer_arithmetic test { defer diagnostics.deinit(&diagnostic_store) module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) code := diagnostics.Code.integer_overflow if (i < 2) code = .literal_out_of_range if (i >= 6) code = .invalid_escape @@ -285,7 +296,8 @@ validates_main_and_preserves_first_declaration test { defer diagnostics.deinit(&diagnostic_store) module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) if (i == 3) { try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) @@ -308,7 +320,8 @@ invalid_signatures_trap_without_poisoning_unrelated_main test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect(diagnostics.count(&diagnostic_store) > 0) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) if (i == 0) { try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) @@ -327,7 +340,8 @@ parses_float_context_without_integer_range_limits_and_rejects_float_overflow tes &diagnostic_store, ) defer ir.deinit(&module) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.float, outcome.value.type) try testing.expect_equal(f64(18446744073709551616.0), outcome.value.data.real) @@ -338,7 +352,8 @@ parses_float_context_without_integer_range_limits_and_rejects_float_overflow tes for (".0\n}\n") |byte| try arraylist.append(&source, byte) oversized := try compile_owned(source.items, &diagnostic_store) defer ir.deinit(&oversized) - trapped :: try interpreter.run(mem.c_allocator, &oversized, &diagnostic_store) + trapped := try interpreter.run(mem.c_allocator, &oversized, &diagnostic_store) + defer interpreter.deinit(&trapped) try expect_trap(trapped, &diagnostic_store, .literal_out_of_range) } @@ -354,7 +369,8 @@ accepts_representable_subnormals_and_rejects_underflow_to_zero test { defer diagnostics.deinit(&diagnostic_store) module := try compile_owned(source.items, &diagnostic_store) defer ir.deinit(&module) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) if (count == 307) { try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) try testing.expect(outcome.trap == null) @@ -436,8 +452,11 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test { ` local :: value ` return local `} + `rows proc(value [2][2]string) [2][2]string { return value } `main proc() string { ` computed float :: factor * 2 + ` aggregate :: rows([[text, "second"], ["third", text]]) + ` (aggregate[1])[1] ` remaining := 2 ` for 0..=2 |item| { ` saved :: identity(text) @@ -491,9 +510,11 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test { continue } errdefer ir.deinit(&module) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) - try testing.expect(mem.eql(outcome.value.data.string, "owned\nstring")) + try testing.expect(mem.eql(ir.string_bytes(&module, outcome.value.data.string), "owned\nstring")) + interpreter.deinit(&outcome) ir.deinit(&module) try testing.expect_equal(usize(0), state.live) succeeded = true @@ -520,7 +541,8 @@ recursive_factorial_and_conditional_returns_execute_after_frontend_cleanup test module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(120), outcome.value.data.integer) } @@ -564,7 +586,8 @@ nested_branches_joins_calls_and_lazy_globals_preserve_execution_position test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) } @@ -594,7 +617,8 @@ bool_storage_results_and_numeric_comparisons_execute test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect(outcome.value.data.boolean) @@ -618,7 +642,8 @@ conditional_errors_trap_only_when_demanded_and_preserve_original_identity test { defer ir.deinit(&module) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) if (index == 0 or index == 2 or index == 4) { try testing.expect(outcome.trap == null) if (index == 4) try testing.expect_equal(ir.Type.void, outcome.value.type) @@ -646,7 +671,8 @@ missing_return_does_not_replace_reached_arm_error test { defer diagnostics.deinit(&diagnostic_store) module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) if (index == 2) { try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) @@ -678,7 +704,8 @@ nested_locals_are_predeclared_before_any_arm_executes test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try expect_trap(outcome, &diagnostic_store, .read_before_initialization) } @@ -701,7 +728,8 @@ iterative_factorial_executes_from_owned_ir_after_frontend_cleanup test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(i64(120), outcome.value.data.integer) @@ -722,7 +750,8 @@ zero_nested_early_return_and_predicate_call_loops_execute test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(i64(42), outcome.value.data.integer) @@ -743,7 +772,8 @@ float_bool_string_and_compatible_function_updates_execute test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(expected_types[index], outcome.value.type) match index { @@ -768,7 +798,8 @@ forward_assignments_check_initialization_after_evaluating_the_rhs test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try expect_trap(outcome, &diagnostic_store, codes[index]) diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "later")) @@ -803,7 +834,8 @@ loop_and_reentered_arm_locals_are_uninitialized_before_later_declarations test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try expect_trap(outcome, &diagnostic_store, codes[index]) diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "later")) @@ -832,7 +864,8 @@ invalid_assignments_and_loop_conditions_preserve_original_diagnostics test { try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? try testing.expect_equal(codes[index], original.code) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) } @@ -853,7 +886,8 @@ zero_iteration_body_errors_are_checked_but_not_executed test { defer ir.deinit(&module) try testing.expect(diagnostics.count(&diagnostic_store) > 0) count :: diagnostics.count(&diagnostic_store) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(count, diagnostics.count(&diagnostic_store)) @@ -878,7 +912,8 @@ loop_returns_do_not_guarantee_return_and_assignment_arithmetic_still_checks_over diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? try testing.expect_equal(diagnostics.Code.missing_return, diagnostic.code) } else try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) if (index == 0) { try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) @@ -905,9 +940,11 @@ bounded_pure_loops_do_not_allocate_per_iteration_and_release_runtime_storage tes try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - outcome :: try interpreter.run(allocator, &module, &diagnostic_store) + outcome := try interpreter.run(allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(expected[index], outcome.value.data.integer) + interpreter.deinit(&outcome) try testing.expect_equal(usize(0), state.live) if (index == 0) allocations = usize(64) - state.remaining else try testing.expect_equal(allocations, usize(64) - state.remaining) @@ -945,7 +982,8 @@ boolean_truth_tables_nesting_negation_and_precedence_execute test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(expected[index], outcome.value.data.boolean) @@ -970,7 +1008,8 @@ branchful_calls_preserve_earlier_temps_nested_arguments_and_function_callees tes module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) } @@ -995,7 +1034,8 @@ short_circuit_source_errors_remain_checked_but_skipped_in_every_consumer test { defer ir.deinit(&module) count :: diagnostics.count(&diagnostic_store) try testing.expect(count > 0) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect(outcome.value.data.boolean) @@ -1025,7 +1065,8 @@ demanded_boolean_source_errors_keep_the_original_diagnostic_and_operand_span tes operand []u8 :: if (index >= 2 and index <= 4) "1" else "absent" try testing.expect_equal(code, original.code) try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], operand)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) } @@ -1042,7 +1083,8 @@ branchful_call_arguments_evaluate_left_to_right_before_later_errors test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) original :: diagnostics.get(&diagnostic_store, outcome.trap?)? try testing.expect_equal(diagnostics.Code.unknown_name, original.code) try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], "first_absent")) @@ -1064,7 +1106,8 @@ short_circuit_protects_uninitialized_reads_global_cycles_and_overflow test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect(outcome.value.data.boolean) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) @@ -1086,7 +1129,8 @@ demanded_boolean_runtime_errors_keep_the_instruction_span test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try expect_trap(outcome, &diagnostic_store, codes[index]) diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], spans[index])) @@ -1114,7 +1158,8 @@ composed_loop_guards_recompute_and_overwrite_synthetic_results test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) } @@ -1150,14 +1195,16 @@ lazy_branchful_global_initializers_and_calls_release_every_runtime_allocation te for (usize(0)..usize(64)) |limit| { state := AllocationState{ remaining = limit } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - outcome :: interpreter.run(allocator, &module, &diagnostic_store) catch |error| { + outcome := interpreter.run(allocator, &module, &diagnostic_store) catch |error| { try testing.expect(error == .out_of_memory) failures += 1 try testing.expect_equal(usize(0), state.live) continue } + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect(outcome.value.data.boolean) + interpreter.deinit(&outcome) try testing.expect_equal(usize(0), state.live) succeeded = true break @@ -1181,7 +1228,8 @@ skipped_rhs_does_not_hide_a_demanded_outer_type_mismatch test { defer ir.deinit(&module) count :: diagnostics.count(&diagnostic_store) try testing.expect_equal(2, count) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try expect_trap(outcome, &diagnostic_store, codes[index]) try testing.expect_equal(count, diagnostics.count(&diagnostic_store)) } @@ -1199,7 +1247,8 @@ break_and_continue_skip_iterations_exit_early_and_restore_outer_targets test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) } @@ -1223,7 +1272,8 @@ continue_restarts_composed_calling_guards_after_branchful_body_calls test { module := try compile_owned(source.items, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) } @@ -1247,7 +1297,8 @@ reached_outside_controls_preserve_diagnostic_identity_and_keyword_span test { original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? try testing.expect_equal(codes[index], original.code) try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], keywords[index])) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) } @@ -1270,7 +1321,8 @@ untaken_outside_controls_remain_checked_without_trapping test { try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? try testing.expect_equal(codes[index], original.code) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) @@ -1292,7 +1344,8 @@ source_errors_after_loop_transfers_stay_checked_but_unreached test { try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? try testing.expect_equal(diagnostics.Code.unknown_name, original.code) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) @@ -1310,7 +1363,8 @@ traps_before_loop_controls_keep_the_original_error test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try expect_trap(outcome, &diagnostic_store, .unknown_name) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) @@ -1340,7 +1394,8 @@ owned_integer_ranges_execute_exclusive_inclusive_empty_signed_and_discard_forms module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(expected[index], outcome.value.data.integer) @@ -1372,7 +1427,8 @@ range_bounds_calls_and_globals_are_frozen_before_branchful_body_calls test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) } @@ -1389,7 +1445,8 @@ range_break_continue_and_nested_for_while_restore_lexical_targets test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) } @@ -1415,7 +1472,8 @@ range_body_and_arm_locals_reset_before_continued_later_declarations test { module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try expect_trap(outcome, &diagnostic_store, codes[index]) diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "later")) @@ -1443,7 +1501,8 @@ untaken_range_errors_and_errors_after_controls_remain_diagnostic_only test { defer ir.deinit(&module) count :: diagnostics.count(&diagnostic_store) try testing.expect(count > 0) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(count, diagnostics.count(&diagnostic_store)) @@ -1476,7 +1535,8 @@ demanded_range_header_capture_and_body_errors_keep_original_diagnostic_identity try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? try testing.expect_equal(codes[index], original.code) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) } @@ -1495,7 +1555,8 @@ range_bounds_evaluate_left_to_right_before_header_errors_and_body_overflow_keeps module := try compile_owned(source, &diagnostic_store) defer ir.deinit(&module) before :: diagnostics.count(&diagnostic_store) - outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) code diagnostics.Code :: if (index == 3) .unknown_name else .integer_overflow try expect_trap(outcome, &diagnostic_store, code) diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? @@ -1526,11 +1587,302 @@ range_runtime_allocations_do_not_grow_with_iterations test { try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } - outcome :: try interpreter.run(allocator, &module, &diagnostic_store) + outcome := try interpreter.run(allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) try testing.expect(outcome.trap == null) try testing.expect_equal(expected[index], outcome.value.data.integer) + interpreter.deinit(&outcome) try testing.expect_equal(usize(0), state.live) if (index == 0) allocations = usize(64) - state.remaining else try testing.expect_equal(allocations, usize(64) - state.remaining) } } + +array_mutation_copies_rows_wholes_and_by_value_calls test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `seed :: later + `later [2]mut [2]mut i64 :: [[1, 2], [3, 4]] + `identity proc(value [2]mut [2]mut i64) [2]mut [2]mut i64 { return value } + `changed proc(value [2]mut [2]mut i64) [2]mut [2]mut i64 { + ` copy := value + ` (copy[0])[0] = 20 + ` return copy + `} + `main proc() bool { + ` a := identity(identity(seed)) + ` row := a[0] + ` row[0] = 9 + ` b := changed(identity(a)) + ` (b[1])[1] = 30 + ` c := a + ` c = b + ` (b[0])[1] = 40 + ` a[1] = row + ` row[1] = 50 + ` count usize :: 2 + ` sum := 0 + ` for 0..count |i| { + ` for 0..count |j| { sum = sum + a[i][j] } + ` } + ` return sum == 14 and seed[0][0] == 1 and seed[1][1] == 4 and c[0][0] == 20 and c[0][1] == 2 and c[1][1] == 30 + `} + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect(outcome.value.data.boolean) +} + +array_context_inference_empty_shapes_and_scalar_leaf_families test { + sources [8][]u8 :: [ + "main proc() bool {\n a [_]mut i64 := [1, 2, 3]\n a[1] = 9\n return a[1] == 9\n}\n", + "main proc() bool {\n a [0]bool :: []\n b :: []\n return true\n}\n", + "main proc() bool {\n a [2]mut f64 := [1, 2.5]\n a[0] = 3.5\n return a[0] + a[1] == 6.0\n}\n", + "main proc() bool {\n a [2]mut bool := [false or true, true and false]\n a[1] = true\n return a[0] and a[1]\n}\n", + "main proc() string {\n a [2]mut string := [\"one\", \"two\"]\n b := a\n a[1] = \"three\"\n return b[1]\n}\n", + "plus proc(n int) int { return n + 1 }\nminus proc(n int) int { return n - 1 }\nmain proc() bool {\n a :: [plus, minus]\n return a[0](41) == 42 and a[1](43) == 42\n}\n", + "make proc() [2]i64 { return [20, 22] }\nmain proc() bool {\n return make()[0] + ([20, 22])[1] == 42\n}\n", + "count :: alias + 1\nalias :: 1\nmain proc() bool {\n a [count][0]bool :: [[], []]\n return true\n}\n", + ] + for (sources) |source| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + if (outcome.value.type == .string) { + try testing.expect(mem.eql(ir.string_bytes(&module, outcome.value.data.string), "two")) + } else { + try testing.expect(outcome.value.data.boolean) + } + } +} + +array_literal_scratch_handles_nested_calls_and_branchful_elements test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `identity proc(value [2]bool) [2]bool { return value } + `pair proc(left [2]bool, right [2]bool) [2][2]bool { return [left, right] } + `main proc() bool { + ` value :: pair(identity([true or false, false and true]), identity(identity([false or true, true and !false]))) + ` return value[0][0] and !value[0][1] and value[1][0] and value[1][1] + `} + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect(outcome.value.data.boolean) +} + +array_bounds_roots_and_mutability_trap_at_demanded_positions test { + sources [12][]u8 :: [ + "main proc() int {\n a :: [1]\n return a[1]\n}\n", + "main proc() int {\n a [0]i64 :: []\n return a[0]\n}\n", + "main proc() int {\n a [1]mut i64 := [1]\n a[18446744073709551615] = 2\n return 0\n}\n", + "main proc() int {\n a [1]mut i64 :: [1]\n a[0] = 2\n return 0\n}\n", + "main proc() int {\n a := [1]\n a[0] = 2\n return 0\n}\n", + "a [1]mut i64 :: [1]\nmain proc() int {\n a[0] = 2\n return 0\n}\n", + "bad proc(a [1]mut i64) int {\n a[0] = 2\n return 0\n}\nmain proc() int {\n return bad([1])\n}\n", + "main proc() int {\n a [1]mut [1]i64 := [[1]]\n (a[0])[0] = 2\n return 0\n}\n", + "main proc() int {\n a [1][1]mut i64 := [[1]]\n (a[0])[0] = 2\n return 0\n}\n", + "a [1]i64 :: b\nb [1]i64 :: a\nmain proc() int {\n return a[0]\n}\n", + "main proc() int {\n before :: after[0]\n after [1]i64 :: [1]\n return before\n}\n", + "main proc() int {\n [1]i64\n return 0\n}\n", + ] + codes [12]diagnostics.Code :: [ + .index_out_of_bounds, .index_out_of_bounds, .index_out_of_bounds, + .invalid_index_assignment, .invalid_index_assignment, .invalid_index_assignment, + .invalid_index_assignment, .invalid_index_assignment, .invalid_index_assignment, + .initialization_cycle, .read_before_initialization, .type_not_value, + ] + for (sources) |source, i| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try expect_trap(outcome, &diagnostic_store, codes[i]) + } +} + +array_assignment_rhs_and_nested_bounds_preserve_error_order_and_identity test { + sources [5][]u8 :: [ + "rhs proc() i64 { return absent_rhs }\nmain proc() int {\n a [0]mut i64 := []\n a[absent_index] = rhs()\n return 0\n}\n", + "index proc() usize { return absent_inner }\nmain proc() int {\n a [1][1]i64 :: [[1]]\n return a[2][index()]\n}\n", + "rhs proc() i64 { return absent_rhs }\nmain proc() int {\n a :: [1]\n a[0] = rhs()\n return 0\n}\n", + "main proc() int {\n a [0]mut i64 := []\n a[0] = 9223372036854775807 + 1\n return 0\n}\n", + "main proc() int {\n a [1]mut i64 := [1]\n a[absent_index] = absent_rhs\n return 0\n}\n", + ] + codes [5]diagnostics.Code :: [.unknown_name, .index_out_of_bounds, .unknown_name, .integer_overflow, .unknown_name] + for (sources) |source, i| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + before :: diagnostics.count(&diagnostic_store) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try expect_trap(outcome, &diagnostic_store, codes[i]) + if (i == 0 or i == 2 or i == 4) { + diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? + try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "absent_rhs")) + try testing.expect_equal(before, diagnostics.count(&diagnostic_store)) + } + } +} + +usize_arithmetic_and_guarded_ranges_use_unsigned_full_width test { + sources [5][]u8 :: [ + "main proc() bool {\n max usize :: 18446744073709551615\n n := 0\n for max..=max |i| {\n if (i != max) return false\n n = n + 1\n continue\n }\n return n == 1\n}\n", + "main proc() bool {\n max usize :: 18446744073709551615\n zero usize :: 0\n n := 0\n for max..max |_| { n = n + 1 }\n for zero..=zero |_| { n = n + 1 }\n return n == 1\n}\n", + "main proc() bool {\n start usize :: 18446744073709551614\n end usize :: 18446744073709551615\n n := 0\n for start..=end |i| {\n n = n + 1\n continue\n }\n return n == 2\n}\n", + "main proc() bool {\n a usize :: 9223372036854775808\n return a > 9223372036854775807 and a * 1 + 1 - 1 == a\n}\n", + "main proc() bool {\n end usize :: 3\n sum usize := 0\n for 0..end |i| { sum = sum + i }\n return sum == 3\n}\n", + ] + for (sources) |source| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect(outcome.value.data.boolean) + } +} + +aggregate_loop_frames_allocate_once_and_reset_local_freshness test { + sources [2][]u8 :: [ + "main proc() int {\n end usize :: 1\n count := 0\n a [1]mut i64 := [0]\n for 0..end |i| {\n row [1]mut i64 := [a[0]]\n row[0] = row[0] + 1\n a = row\n count = count + 1\n }\n return count + a[0]\n}\n", + "main proc() int {\n end usize :: 10000\n count := 0\n a [1]mut i64 := [0]\n for 0..end |i| {\n row [1]mut i64 := [a[0]]\n row[0] = row[0] + 1\n a = row\n count = count + 1\n }\n return count + a[0]\n}\n", + ] + expected [2]i64 :: [2, 20000] + allocations := usize(0) + for (sources) |source, i| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + state := AllocationState{ remaining = 64 } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + outcome := try interpreter.run(allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(expected[i], outcome.value.data.integer) + interpreter.deinit(&outcome) + try testing.expect_equal(usize(0), state.live) + if (i == 0) allocations = usize(64) - state.remaining + else try testing.expect_equal(allocations, usize(64) - state.remaining) + } + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned( + "main proc() int {\n end usize :: 2\n for 0..end |i| {\n if (i == 1) return later[0]\n later [1]i64 :: [42]\n }\n return 0\n}\n", + &diagnostic_store, + ) + defer ir.deinit(&module) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try expect_trap(outcome, &diagnostic_store, .read_before_initialization) +} + +escaped_nested_array_returns_survive_frontend_and_frames_and_release_on_oom test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `later [2][2]string :: [["owned\none", "two"], ["three", "four"]] + `first :: identity(later) + `identity proc(value [2][2]string) [2][2]string { return value } + `main proc() [2][2]string { return identity(identity(first)) } + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + succeeded := false + failures := usize(0) + for (usize(0)..usize(512)) |limit| { + state := AllocationState{ remaining = limit } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + outcome := interpreter.run(allocator, &module, &diagnostic_store) catch |error| { + try testing.expect(error == .out_of_memory) + failures += 1 + try testing.expect_equal(usize(0), state.live) + continue + } + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(ir.Type.array, outcome.value.type) + try testing.expect_equal(usize(4), outcome.value.data.elements.len) + try testing.expect(mem.eql(ir.string_bytes(&module, outcome.value.data.elements[0].data.string), "owned\none")) + try testing.expect(mem.eql(ir.string_bytes(&module, outcome.value.data.elements[3].data.string), "four")) + try testing.expect(state.live > 0) + interpreter.deinit(&outcome) + try testing.expect_equal(usize(0), state.live) + succeeded = true + break + } + try testing.expect(succeeded) + try testing.expect(failures > 0) +} + +unsigned_arithmetic_reports_overflow_without_signed_narrowing test { + sources [3][]u8 :: [ + "main proc() usize {\n return 18446744073709551615 + 1\n}\n", + "main proc() usize {\n return 0 - 1\n}\n", + "main proc() usize {\n return 9223372036854775808 * 2\n}\n", + ] + for (sources) |source| { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned(source, &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try expect_trap(outcome, &diagnostic_store, .integer_overflow) + } +} + +array_index_write_checks_initialization_after_rhs test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned( + "main proc() int {\n a[0] = 42\n a [1]mut i64 := [0]\n return 0\n}\n", + &diagnostic_store, + ) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try expect_trap(outcome, &diagnostic_store, .write_before_initialization) +} + +empty_array_entry_results_need_no_escaped_allocation test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned("main proc() [0]bool { return [] }\n", &diagnostic_store) + defer ir.deinit(&module) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + state := AllocationState{ remaining = 64 } + allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } + outcome := try interpreter.run(allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(ir.Type.array, outcome.value.type) + try testing.expect_equal(usize(0), outcome.value.data.elements.len) + try testing.expect_equal(usize(0), state.live) + interpreter.deinit(&outcome) + try testing.expect_equal(usize(0), state.live) +} diff --git a/source/main.hon b/source/main.hon index 0344299..9731218 100644 --- a/source/main.hon +++ b/source/main.hon @@ -13,6 +13,7 @@ import "@source/sema" import "@source/ir" import "@source/lower" import "@source/interpreter" + typecheck :: import "@source/sema/check" test import "@std/mem" @@ -65,6 +66,7 @@ infrastructure_message proc(error DriverError) []u8 { return match error { .out_of_memory: "out of memory" .diagnostic_limit_reached: "diagnostic limit reached" + .storage_limit_reached: "aggregate storage limit reached" .source_too_large: "source file is too large" .out_of_space: "identifier storage is full" .open_failed: "cannot open source file" @@ -133,7 +135,8 @@ run_file proc(allocator mem.Allocator, runtime io.Io, path [;0]u8) i32 ! DriverE defer diagnostics.deinit(&diagnostic_store) module := try compile(allocator, source.items, &diagnostic_store) defer ir.deinit(&module) - outcome :: try interpreter.run(allocator, &module, &diagnostic_store) + outcome := try interpreter.run(allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) if (outcome.trap == null) try print_result(io.stdout(runtime), &module, outcome.value) try report_diagnostics(allocator, io.stderr(runtime), path, source.items, &diagnostic_store, outcome.trap) if (outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1 @@ -142,12 +145,43 @@ run_file proc(allocator mem.Allocator, runtime io.Io, path [;0]u8) i32 ! DriverE @hide:file print_result proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError { + if (value.type == .void) return + try print_value(output, module, value) + try io.print("\n", output, {}) +} + +@hide:file +print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError { match value.type { - .int: try io.print("{}\n", output, {value.data.integer}) - .float: try io.print("{d}\n", output, {value.data.real}) - .bool: try io.print("{}\n", output, {value.data.boolean}) - .string: try io.print("{s}\n", output, {ir.string_bytes(module, value.data.string)}) - .void: {} + .int: try io.print("{}", output, {value.data.integer}) + .usize: try io.print("{}", output, {value.data.unsigned}) + .float: try io.print("{d}", output, {value.data.real}) + .bool: try io.print("{}", output, {value.data.boolean}) + .string: try io.print("{s}", output, {ir.string_bytes(module, value.data.string)}) + .array: { + shape :: module.arrays.items[usize(value.array_type)] + stride usize :: if (shape.element == .array) + module.arrays.items[usize(shape.element_array)].cells + else + 1 + try io.print("[", output, {}) + for 0..(shape.len) |index| { + if (index != 0) try io.print(", ", output, {}) + element := ir.Value{} + if shape.element == .array { + start :: index * stride + element = ir.Value{ + type = .array, + array_type = shape.element_array, + data = ir.ValueData{ elements = value.data.elements[start..(start + stride)] }, + } + } else { + element = value.data.elements[index] + } + try print_value(output, module, element) + } + try io.print("]", output, {}) + } else: unreachable } } diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 144bc84..61b61bc 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -103,7 +103,7 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError { while true { token :: try consume(state, .ident) try arraylist.append(&names, token.span) - if next_is(state, .ident) break + if (next_is(state, .ident) or next_is(state, .open_bracket)) break _ = try consume(state, .comma) } @@ -169,13 +169,11 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError { @hide:file parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { - if (next_is(state, .ident) and next_n_is(state, 1, .equal)) { - return try parse_assignment(state, allow_else) - } if ( next_is(state, .ident) and ( next_n_is(state, 1, .ident) + or (next_n_is(state, 1, .open_bracket) and array_starts_type(state)) or next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) ) @@ -188,8 +186,9 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError .for: return try parse_for(state, allow_else) .return: return try parse_return(state, allow_else) .break, .continue: return try parse_loop_control(state, allow_else) - .ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren: { + .ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket: { expression :: try parse_expr(state) + if next_is(state, .equal) return try parse_assignment(state, expression, allow_else) return try ast.add_expression_stmt(&state.builder, expression) } else: return .unexpected_token @@ -239,9 +238,7 @@ parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseErr } @hide:file -parse_assignment proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { - name :: try consume(state, .ident) - target :: try ast.add_identifier(&state.builder, name.span) +parse_assignment proc(state @mut State, target ast.NodeId, allow_else bool) ast.NodeId ! ParseError { _ = try consume(state, .equal) value :: try parse_expr(state) if ( @@ -255,7 +252,7 @@ parse_assignment proc(state @mut State, allow_else bool) ast.NodeId ! ParseError end :: state.tokens[state.next_token - 1].span.end return try ast.add_assignment_stmt( &state.builder, - ast.Span{ start = name.span.start, end = end }, + ast.Span{ start = ast.builder_span(&state.builder, target).start, end = end }, target, value, ) @@ -454,8 +451,8 @@ parse_arm proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { .unexpected_token: {} else: return err } - diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) - span :: synchronize_arm(state, start) + diagnostic :: try unexpected_diagnostic(state) + span :: synchronize(state, start, true, true, false) yield try ast.add_invalid_stmt(&state.builder, span, diagnostic) } statements [1]ast.NodeId :: [statement] @@ -466,28 +463,11 @@ parse_arm proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { ) } -@hide:file -synchronize_arm proc(state @mut State, start usize) ast.Span { - first :: state.tokens[start].span - end := first.end - depth usize := 0 - while !next_is(state, .eof) { - if ( - depth == 0 - and (next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .else)) - ) break - if next_is(state, .open_curly) depth += 1 - if (next_is(state, .close_curly) and depth > 0) depth -= 1 - end = current(state).span.end - state.next_token += 1 - } - return ast.Span{ start = first.start, end = end } -} @hide:file parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError { name :: try consume(state, .ident) - type_id ?ast.NodeId :: if next_is(state, .ident) + type_id ?ast.NodeId :: if (next_is(state, .ident) or next_is(state, .open_bracket)) try parse_type(state) else null @@ -508,9 +488,29 @@ parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError { @hide:file parse_type proc(state @mut State) ast.NodeId ! ParseError { - if (!next_is(state, .ident)) return .unexpected_token - token :: current(state) - state.next_token += 1 + if next_is(state, .open_bracket) { + open :: try consume(state, .open_bracket) + count ?ast.NodeId := null + if ( + next_is(state, .ident) + and current(state).span.end == current(state).span.start + 1 + and state.builder.source[usize(current(state).span.start)] == '_' + ) { + state.next_token += 1 + } else { + count = try parse_expr(state) + } + _ = try consume(state, .close_bracket) + mutable :: next_is(state, .mut) + if (mutable) state.next_token += 1 + element :: try parse_type(state) + return try ast.add_array_type( + &state.builder, + ast.Span{ start = open.span.start, end = ast.builder_span(&state.builder, element).end }, + count, element, mutable, + ) + } + token :: try consume(state, .ident) return try ast.add_identifier(&state.builder, token.span) } @@ -544,7 +544,12 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par skip_continuation_newlines(state) token = current(state) if (token.kind == .open_paren) { - left = try parse_call(state, left) + left = try parse_call(state, left, state.tokens[expression_start].span.start) + left_grouped = false + continue + } + if (token.kind == .open_bracket) { + left = try parse_index(state, left, state.tokens[expression_start].span.start) left_grouped = false continue } @@ -603,7 +608,7 @@ validate_range_bound proc( } @hide:file -parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError { +parse_call proc(state @mut State, callee ast.NodeId, start u32) ast.NodeId ! ParseError { _ = try consume(state, .open_paren) state.paren_depth += 1 defer state.paren_depth -= 1 @@ -622,7 +627,7 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError { return try ast.add_call( &state.builder, ast.Span{ - start = ast.builder_span(&state.builder, callee).start, + start = start, end = close.span.end, }, callee, @@ -630,10 +635,108 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError { ) } +@hide:file +parse_index proc(state @mut State, base ast.NodeId, start u32) ast.NodeId ! ParseError { + _ = try consume(state, .open_bracket) + index :: try parse_expr(state) + if !next_is(state, .close_bracket) { + diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) + return try ast.add_invalid_expr( + &state.builder, + ast.Span{ + start = start, + end = ast.builder_span(&state.builder, index).end, + }, + diagnostic, + ) + } + close :: try consume(state, .close_bracket) + return try ast.add_index( + &state.builder, + ast.Span{ start = start, end = close.span.end }, + base, index, + ) +} + +@hide:file +array_starts_type proc(state @State) bool { + depth usize := 0 + for (state.next_token)..(state.tokens.len) |i| { + kind :: state.tokens[i].kind + if (kind == .open_bracket) depth += 1 + if (kind == .close_bracket) { + depth -= 1 + if (depth == 0) { + if (i + 1 >= state.tokens.len) return false + next :: state.tokens[i + 1].kind + return next == .ident or next == .mut or next == .open_bracket + } + } + if (kind == .open_curly or kind == .close_curly or kind == .eof) return false + } + return false +} + +@hide:file +parse_array_literal proc(state @mut State) ast.NodeId ! ParseError { + open :: try consume(state, .open_bracket) + elements arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator) + defer arraylist.deinit(&elements) + after_open :: state.next_token + skip_newlines(state) + if named_binding_starts(state) { + state.next_token = after_open + return try invalid_array_literal(state, open.span.start, open.span.end) + } + while !next_is(state, .close_bracket) { + element :: try parse_expr(state) + try arraylist.append(&elements, element) + after_element :: state.next_token + skip_newlines(state) + if next_is(state, .close_bracket) break + if !next_is(state, .comma) { + state.next_token = after_element + return try invalid_array_literal(state, open.span.start, ast.builder_span(&state.builder, element).end) + } + state.next_token += 1 + after_comma :: state.next_token + skip_newlines(state) + if named_binding_starts(state) { + state.next_token = after_comma + return try invalid_array_literal(state, open.span.start, ast.builder_span(&state.builder, element).end) + } + } + close :: try consume(state, .close_bracket) + return try ast.add_array_literal( + &state.builder, ast.Span{ start = open.span.start, end = close.span.end }, elements.items, + ) +} + +@hide:file +invalid_array_literal proc(state @mut State, start, end u32) ast.NodeId ! ParseError { + diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) + return try ast.add_invalid_expr(&state.builder, ast.Span{ start = start, end = end }, diagnostic) +} + +@hide:file +named_binding_starts proc(state @State) bool { + return ( + next_is(state, .ident) + and ( + next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) + or next_n_is(state, 1, .ident) or next_n_is(state, 1, .proc) + ) + ) +} + @hide:file parse_primary proc(state @mut State) ast.NodeId ! ParseError { token :: current(state) match token.kind { + .open_bracket: { + if array_starts_type(state) return try parse_type(state) + return try parse_array_literal(state) + } .open_paren: { state.next_token += 1 state.paren_depth += 1 @@ -709,6 +812,7 @@ is_call_boundary proc(kind lexer.TokenKind) bool { is_expression_recovery_boundary proc(kind lexer.TokenKind) bool { return ( kind == .comma + or kind == .close_bracket or kind == .and or kind == .or or kind == .pipe @@ -725,14 +829,8 @@ skip_operand_newlines proc(state @mut State) void { if (kind != .close_paren and is_expression_boundary(kind)) state.next_token = start # a missing operand must not consume the declaration or assignment on the next line. if ( - next_is(state, .ident) - and ( - next_n_is(state, 1, .double_colon) - or next_n_is(state, 1, .colon_equal) - or next_n_is(state, 1, .equal) - or next_n_is(state, 1, .ident) - or next_n_is(state, 1, .proc) - ) + named_binding_starts(state) + or (next_is(state, .ident) and next_n_is(state, 1, .equal)) ) state.next_token = start } @@ -744,6 +842,8 @@ skip_continuation_newlines proc(state @mut State) void { kind :: current(state).kind if ( kind != .close_paren and kind != .comma and kind != .open_paren + and kind != .close_bracket and kind != .open_bracket + and kind != .range and kind != .range_inclusive and infix_binding_power(kind) == null ) state.next_token = start } @@ -753,8 +853,8 @@ recover_decl proc( state @mut State, start usize, ) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) { - diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) - span :: synchronize(state, start, false) + diagnostic :: try unexpected_diagnostic(state) + span :: synchronize(state, start, false, false, true) return try ast.add_invalid_decl(&state.builder, span, diagnostic) } @@ -763,31 +863,91 @@ recover_statement proc( state @mut State, start usize, ) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) { - diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) - span :: synchronize(state, start, true) + diagnostic :: try unexpected_diagnostic(state) + span :: synchronize(state, start, true, false, true) return try ast.add_invalid_stmt(&state.builder, span, diagnostic) } @hide:file -synchronize proc(state @mut State, start usize, stop_at_curly bool) ast.Span { +synchronize proc( + state @mut State, start usize, stop_at_curly, allow_else, consume_newline bool, +) ast.Span { first :: state.tokens[start].span end := first.end - depth usize := 0 + curly usize := 0 + paren usize := 0 + square usize := 0 + for (start..state.next_token) |i| { + kind :: state.tokens[i].kind + if (kind == .open_paren) paren += 1 + if (kind == .close_paren and paren > 0) paren -= 1 + if (kind == .open_bracket) square += 1 + if (kind == .close_bracket and square > 0) square -= 1 + } while !next_is(state, .eof) { - if (depth == 0 and next_is(state, .newline)) break - if (stop_at_curly and depth == 0 and next_is(state, .close_curly)) break - if (stop_at_curly and next_is(state, .open_curly)) depth += 1 - if (stop_at_curly and next_is(state, .close_curly) and depth > 0) depth -= 1 + if (curly == 0) { + if (stop_at_curly and next_is(state, .close_curly)) break + if (allow_else and next_is(state, .else)) break + if ( + next_is(state, .newline) + and !recovery_closes_delimiters(state, paren, square) + ) break + } + kind :: current(state).kind + if (kind == .open_curly) curly += 1 + if (kind == .close_curly and curly > 0) curly -= 1 + if (kind == .open_paren) paren += 1 + if (kind == .close_paren and paren > 0) paren -= 1 + if (kind == .open_bracket) square += 1 + if (kind == .close_bracket and square > 0) square -= 1 end = current(state).span.end state.next_token += 1 } - if next_is(state, .newline) { + if (consume_newline and next_is(state, .newline)) { end = current(state).span.end state.next_token += 1 } return ast.Span{ start = first.start, end = end } } +@hide:file +recovery_closes_delimiters proc(state @State, paren_depth, square_depth usize) bool { + if (paren_depth == 0 and square_depth == 0) return false + paren := paren_depth + square := square_depth + for (state.next_token)..(state.tokens.len) |i| { + kind :: state.tokens[i].kind + match kind { + .open_curly, .close_curly, .eof, .if, .else, .while, .for, + .return, .break, .continue: return false + .ident: { + if (i + 1 < state.tokens.len) { + next :: state.tokens[i + 1].kind + if (next == .double_colon or next == .colon_equal or next == .ident or next == .proc) { + return false + } + } + } + else: {} + } + if (kind == .open_paren) paren += 1 + if (kind == .close_paren and paren > 0) paren -= 1 + if (kind == .open_bracket) square += 1 + if (kind == .close_bracket and square > 0) square -= 1 + if (paren == 0 and square == 0) return true + } + return false +} + +@hide:file +unexpected_diagnostic proc(state @mut State) diagnostics.DiagnosticId ! (mem.AllocError | diagnostics.CapacityError) { + token :: current(state) + if (token.kind == .invalid and token.data.diagnostic != diagnostics.no_diagnostic) { + return token.data.diagnostic + } + return try diagnostics.add(state.diagnostics, .unexpected_token, token.span) +} + @hide:file skip_newlines proc(state @mut State) void { while next_is(state, .newline) state.next_token += 1 diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index a1934a2..ad080d7 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -1200,3 +1200,201 @@ range_loop_preserves_nearest_else_and_canonical_body test { try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, call_loop.iterable)) try testing.expect_equal(1, ast.block_statement_count(&tree, call_loop.body)) } + +parses_recursive_array_annotations_literals_and_grouped_parameters 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 :: + `matrix [2]mut [width]mut i64 := [ + ` [1, 2,], + ` [3, 4], + `] + `inferred [_]f64 :: [1.0,] + `empty :: [] + `width usize :: 2 + `choose proc(a, b [width]mut [2]i64, index usize) [_]f64 { + ` return [] + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + try testing.expect_equal(5, ast.package_decl_count(&tree)) + declaration :: ast.statement_decl(&tree, ast.package_decl(&tree, 0)) + outer :: ast.array_type(&tree, declaration.type?) + try testing.expect(outer.mutable) + try testing.expect_equal(ast.ExprKind.literal_int, ast.expr_kind(&tree, outer.count?)) + inner :: ast.array_type(&tree, outer.element) + try testing.expect(inner.mutable) + try testing.expect_equal(ast.ExprKind.identifier, ast.expr_kind(&tree, inner.count?)) + try testing.expect_equal("i64", ast.source_text(&tree, ast.node_span(&tree, inner.element))) + try testing.expect_equal(2, ast.array_element_count(&tree, declaration.initializer)) + for (0..ast.array_element_count(&tree, declaration.initializer)) |i| { + try testing.expect_equal(2, ast.array_element_count( + &tree, ast.array_element(&tree, declaration.initializer, i), + )) + } + inferred :: ast.statement_decl(&tree, ast.package_decl(&tree, 1)) + try testing.expect_equal(null, ast.array_type(&tree, inferred.type?).count) + try testing.expect_equal(1, ast.array_element_count(&tree, inferred.initializer)) + empty :: ast.statement_decl(&tree, ast.package_decl(&tree, 2)) + try testing.expect_equal(0, ast.array_element_count(&tree, empty.initializer)) + function :: ast.package_decl(&tree, 4) + try testing.expect_equal(3, ast.function_param_count(&tree, function)) + first :: ast.param(&tree, ast.function_param(&tree, function, 0)) + second :: ast.param(&tree, ast.function_param(&tree, function, 1)) + try testing.expect_equal(first.type, second.type) + try testing.expect_equal(ast.ExprKind.array_type, ast.expr_kind(&tree, first.type)) + try testing.expect_equal(null, ast.array_type(&tree, ast.function_decl(&tree, function).return_type).count) +} + +parses_postfix_indices_and_general_assignment_targets 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 :: -factory()[position()][0] + ([1, 2])[0] + `main proc() void { + ` (matrix[1])[0] = rhs()[0] + ` (matrix) = [[1]] + ` 1 = 2 + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + expression :: ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer + add :: ast.binary_expr(&tree, expression) + negative :: ast.unary_expr(&tree, add.left) + outer :: ast.index_expr(&tree, negative.operand) + inner :: ast.index_expr(&tree, outer.base) + try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, inner.base)) + try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, inner.index)) + try testing.expect_equal(ast.ExprKind.literal_int, ast.expr_kind(&tree, outer.index)) + literal_index :: ast.index_expr(&tree, add.right) + try testing.expect_equal(ast.ExprKind.array_literal, ast.expr_kind(&tree, literal_index.base)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body + try testing.expect_equal(3, ast.block_statement_count(&tree, body)) + assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 0)) + target :: ast.index_expr(&tree, assignment.target) + try testing.expect_equal(ast.ExprKind.index, ast.expr_kind(&tree, target.base)) + try testing.expect_equal(ast.ExprKind.index, ast.expr_kind(&tree, assignment.value)) + grouped :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 1)) + try testing.expect_equal(ast.ExprKind.identifier, ast.expr_kind(&tree, grouped.target)) + invalid_target :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 2)) + try testing.expect_equal(ast.ExprKind.literal_int, ast.expr_kind(&tree, invalid_target.target)) +} + +mirrors_array_type_and_leading_declaration_ambiguities 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 :: + `ambiguous :: [1, 2][0] + `shape :: [1]mut [2]i64 + `main proc() void { + ` matrix[1][0] = 9 + ` (matrix[1])[0] = 9 + ` result :: matrix[1][0] + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) + try testing.expect_equal(3, ast.package_decl_count(&tree)) + try testing.expect_equal(ast.DeclKind.invalid, ast.decl_kind(&tree, ast.package_decl(&tree, 0))) + shape :: ast.statement_decl(&tree, ast.package_decl(&tree, 1)).initializer + try testing.expect_equal(ast.ExprKind.array_type, ast.expr_kind(&tree, shape)) + try testing.expect_equal(ast.ExprKind.array_type, ast.expr_kind(&tree, ast.array_type(&tree, shape).element)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body + try testing.expect_equal(3, ast.block_statement_count(&tree, body)) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 0))) + try testing.expect_equal(ast.StmtKind.assignment_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 1))) + result :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 2)).initializer + try testing.expect_equal(ast.ExprKind.index, ast.expr_kind(&tree, result)) +} + +preserves_array_error_ids_control_bodies_and_following_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 :: + `main proc() void { + ` bad [$]i64 :: [$, 2] + ` while (items[0) { break } + ` for items[0 |i| { continue } + ` unfinished :: [1, + ` kept :: [] + `} + `tail :: 4 + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(2, ast.package_decl_count(&tree)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + try testing.expect_equal(5, ast.block_statement_count(&tree, body)) + bad :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 0)) + count :: ast.array_type(&tree, bad.type?).count? + element :: ast.array_element(&tree, bad.initializer, 0) + try testing.expect_equal(diagnostics.DiagnosticId(0), ast.invalid(&tree, count).diagnostic) + try testing.expect_equal(diagnostics.DiagnosticId(1), ast.invalid(&tree, element).diagnostic) + while_loop :: ast.while_stmt(&tree, ast.block_statement(&tree, body, 1)) + try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, while_loop.condition)) + try testing.expect_equal(ast.StmtKind.break_stmt, ast.stmt_kind( + &tree, ast.block_statement(&tree, while_loop.body, 0), + )) + for_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 2)) + try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, for_loop.iterable)) + try testing.expect_equal(ast.StmtKind.continue_stmt, ast.stmt_kind( + &tree, ast.block_statement(&tree, for_loop.body, 0), + )) + unfinished :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 3)) + try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, unfinished.initializer)) + kept :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 4)) + try testing.expect_equal("kept", ast.source_text(&tree, kept.name)) + try testing.expect_equal(0, ast.array_element_count(&tree, kept.initializer)) +} + +recovers_nested_square_and_parenthesis_delimiters_without_consuming_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 :: + `main proc() void { + ` bad [call(1, + ` 2)]i64 = [3] + ` kept :: [] + ` missing [2 + ` sibling :: [4] + `} + `tail :: 5 + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect(diagnostics.count(&diagnostic_store) > 0) + try testing.expect_equal(2, ast.package_decl_count(&tree)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + try testing.expect_equal(4, ast.block_statement_count(&tree, body)) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 0))) + kept :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 1)) + sibling :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 3)) + try testing.expect_equal(0, ast.array_element_count(&tree, kept.initializer)) + try testing.expect_equal(1, ast.array_element_count(&tree, sibling.initializer)) +} diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index f1a54e4..8cb4a8a 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -15,21 +15,35 @@ TypeKind :: enum { void, function, range, + array, + usize, +} + +ArrayId :: distinct u32 +no_array :: maxval!(ArrayId) + +ArrayType :: struct { + element Type + len usize + mutable bool } Type :: struct { kind TypeKind = .none function ast.NodeId = ast.NodeId(maxval!(u32)) diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic + array_type ArrayId = no_array + range_element TypeKind = .none } @hide:file Storage :: struct { types []mut Type + arrays arraylist.ArrayList(ArrayType) allocator mem.Allocator } -#! owns node-indexed types and retains its allocator; release with deinit. +#! owns node-indexed types and interned array descriptors; release with deinit. #! function IDs refer to the caller-owned AST, not checker-owned signatures. Result :: struct { storage Storage } @@ -68,6 +82,13 @@ Cell :: struct { local bool = false assignment_value bool = false for_iterable bool = false + expected Type = Type{} + visiting bool = false + constant_visiting bool = false + count_declaration bool = false + literal ast.NodeId = ast.NodeId(maxval!(u32)) + producer ast.NodeId = ast.NodeId(maxval!(u32)) + capture ast.NodeId = ast.NodeId(maxval!(u32)) first_edge usize = no_index return_annotation ast.NodeId = ast.NodeId(maxval!(u32)) } @@ -86,12 +107,13 @@ State :: struct { types []mut Type cells []mut Cell edges arraylist.ArrayList(Edge) + arrays arraylist.ArrayList(ArrayType) collecting_assignment bool = false has_assignments bool = false } #! borrows the AST, resolution, and diagnostic store only during checking. -#! temporary constraints are freed on every exit; Result owns only its type table. +#! temporary constraints are freed on every exit; Result owns types and array descriptors. check proc( allocator mem.Allocator, tree @ast.Tree, @@ -115,9 +137,15 @@ check proc( types = types, cells = cells, edges = arraylist.init(allocator), + arrays = arraylist.init(allocator), } defer arraylist.deinit(&state.edges) + errdefer arraylist.deinit(&state.arrays) + for 0..ast.package_decl_count(tree) |i| { + id :: ast.package_decl(tree, i) + if (ast.decl_kind(tree, id) == .statement) cells[usize(id)].count_declaration = true + } for (0..ast.package_decl_count(tree)) |i| { id :: ast.package_decl(tree, i) match ast.decl_kind(tree, id) { @@ -126,18 +154,20 @@ check proc( .invalid: collect_invalid(&state, id) } } - for (0..count) |i| { + # Preserve binding errors before RHS errors propagate. Indexed writability + # waits for shapes; neither check poisons the target's type. + for 0..count |i| { if (cells[i].role != .assignment_stmt) continue target :: ast.assignment_stmt(tree, ast.NodeId(i)).target + if (ast.expr_kind(tree, target) != .identifier) continue binding :: sema.binding(resolution, target) if (binding.kind == .invalid) { cells[i].type = invalid_type(binding.data.diagnostic) - continue + } else if (binding.kind == .declaration and !mutable_target(&state, target)) { + cells[i].type = invalid_type(try diagnostics.add( + diagnostic_store, .invalid_assignment_target, ast.node_span(tree, target), + )) } - if (binding.kind != .declaration or mutable_target(&state, target)) continue - cells[i].type = invalid_type(try diagnostics.add( - diagnostic_store, .invalid_assignment_target, ast.node_span(tree, target), - )) } # aliases share a type variable; explicit annotations and parameters are boundaries. @@ -175,13 +205,12 @@ check proc( if (binding.kind == .declaration) cells[i].type = types[usize(binding.data.declaration)] } } - # function aliases are known before calls; scalar return annotations - # mean a call cannot introduce a function type. + # Calls use the signature result before contextual literal inference. for (0..count) |i| { id :: ast.NodeId(i) if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue callee :: ast.call_expr(tree, id).callee - signature :: group_type(&state, callee) + signature :: try infer_shape(&state, callee, false) if (signature.function != ast.NodeId(maxval!(u32))) { result :: ast.function_decl(tree, signature.function).return_type seed(&state, id, types[usize(result)]) @@ -209,6 +238,9 @@ check proc( types[i] = cells[root(&state, i)].type } # reject nonnumeric operations before connecting otherwise valid inputs. + try apply_contexts(&state, assignment_values, true) + try prepare_shapes(&state, assignment_values, false) + try prepare_ranges(&state, assignment_values, false) for (0..count) |i| { id :: ast.NodeId(i) if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue @@ -253,42 +285,13 @@ check proc( else: {} } } - for (0..count) |i| { + try apply_contexts(&state, assignment_values, false) + try prepare_shapes(&state, assignment_values, false) + try prepare_ranges(&state, assignment_values, true) + for 0..count |i| { id :: ast.NodeId(i) - if (!assignment_values and 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 (!assignment_values and cells[i].role == .return_stmt) { - if (ast.return_stmt(tree, id).value) |value| { - try demand(&state, value, types[usize(cells[i].return_annotation)], .return_type_mismatch) - } - } - if (assignment_values and cells[i].role == .assignment_stmt) { - assignment :: ast.assignment_stmt(tree, id) - if (mutable_target(&state, assignment.target)) { - try demand(&state, assignment.value, types[usize(assignment.target)], .type_mismatch) - } - } - if (cells[i].role != .expression or cells[i].assignment_value != assignment_values 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) - } - } - # integer range demands cannot poison an endpoint's existing float aliases. - for (0..count) |i| { - id :: ast.NodeId(i) - if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .range) continue - expression :: ast.range_expr(tree, id) - try range_bound_context(&state, expression.start) - try range_bound_context(&state, expression.end) + if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .index) continue + try demand(&state, ast.index_expr(tree, id).index, Type{ kind = .usize }, .invalid_index_type) } # defaults follow ordinary program demands, then isolated assignment demands. @@ -296,10 +299,12 @@ check proc( if (root(&state, i) != i or cells[i].assignment_value != assignment_values) 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 + kind TypeKind :: if (cells[i].demand != .none) cells[i].demand else .int cells[i].type = Type{ kind = kind } } } + try prepare_shapes(&state, assignment_values, true) + if (!assignment_values) try specialize_annotations(&state) for (0..count) |i| { id :: ast.NodeId(i) if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue @@ -358,7 +363,7 @@ check proc( types[usize(function.body)] = invalid_type(diagnostic) } } - return Result{ storage = Storage{ types = types, allocator = allocator } } + return Result{ storage = Storage{ types = types, arrays = state.arrays, allocator = allocator } } } #! returns a type without retaining a reference to checker temporaries. @@ -367,9 +372,19 @@ type_of proc(result @Result, id ast.NodeId) Type { return result.storage.types[usize(id)] } -#! releases type storage using the allocator retained by check. +array_type_count proc(result @Result) usize { + return result.storage.arrays.items.len +} + +array_type proc(result @Result, id ArrayId) ArrayType { + debug.assert(usize(id) < result.storage.arrays.items.len) + return result.storage.arrays.items[usize(id)] +} + +#! releases type and descriptor storage using the allocator retained by check. deinit proc(result @mut Result) void { mem.free(result.storage.allocator, result.storage.types) + arraylist.deinit(&result.storage.arrays) } @hide:file @@ -456,6 +471,8 @@ join proc(state @mut State, left, right ast.NodeId) void { 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 + if (state.cells[a].literal == ast.NodeId(maxval!(u32))) state.cells[a].literal = state.cells[b].literal + if (state.cells[a].producer == ast.NodeId(maxval!(u32))) state.cells[a].producer = state.cells[b].producer } @hide:file @@ -475,14 +492,47 @@ collect_invalid proc(state @mut State, id ast.NodeId) void { 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 + if (ast.expr_kind(state.tree, id) == .array_type) { + view :: ast.array_type(state.tree, id) + element := try check_annotation(state, view.element, true) + element_binding :: sema.binding(state.resolution, view.element) + if ((element_binding.kind == .builtin + and (element_binding.data.builtin == .int or element_binding.data.builtin == .float)) + or element.kind == .void or element.kind == .range) { + element = invalid_type(try diagnostics.add(state.diagnostics, .invalid_array_element_type, ast.node_span(state.tree, view.element))) + state.types[usize(view.element)] = element + state.cells[usize(view.element)].type = element + } + len := no_index + type := element + if (view.count) |count| { + try collect_expression(state, count) + if (constant_integer(state, count)) |value| { + if (!value.negative and value.magnitude <= u64(maxval!(usize))) { + len = usize(value.magnitude) + if (len == no_index) type = invalid_type(try diagnostics.add(state.diagnostics, .array_size_overflow, ast.node_span(state.tree, count))) + } else { + type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_array_length, ast.node_span(state.tree, count))) + } + } else { + actual :: group_type(state, count) + type = if (actual.kind == .invalid) actual else invalid_type(try diagnostics.add(state.diagnostics, .invalid_array_length, ast.node_span(state.tree, count))) + } + } + if (type.kind != .invalid) type = try intern_array(state, ArrayType{ element = element, len = len, mutable = view.mutable }) + state.types[usize(id)] = type + state.cells[usize(id)].type = type + return type + } 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 + .int, .i64: .int + .float, .f64: .float + .usize: .usize .bool: .bool .string: .string .void: .void @@ -637,7 +687,10 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b state.cells[usize(statement)].type = invalid_type(diagnostic) } } - if (loop.capture) |capture| try collect_capture(state, capture, loop.body) + if (loop.capture) |capture| { + try collect_capture(state, capture, loop.body) + state.cells[usize(loop.iterable)].capture = capture + } if (loop.index) |index| try collect_capture(state, index, loop.body) try collect_block(state, loop.body, return_annotation, true) try depend(state, loop.body, id) @@ -686,6 +739,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError } .unary: { expression :: ast.unary_expr(tree, id) + if (expression.operator == .negate) state.cells[usize(id)].producer = id if (expression.operator == .logical_not) { state.cells[usize(id)].type = Type{ kind = .bool } } else { @@ -697,6 +751,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError } .binary: { expression :: ast.binary_expr(tree, id) + if (!comparison(expression.operator) and !logical(expression.operator)) state.cells[usize(id)].producer = id if (comparison(expression.operator) or logical(expression.operator)) { state.cells[usize(id)].type = Type{ kind = .bool } } else { @@ -731,13 +786,31 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError try depend(state, argument, id) } } + .array_type: { + state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id))) + } + .array_literal: { + state.cells[usize(id)].literal = id + for 0..ast.array_element_count(tree, id) |i| { + element :: ast.array_element(tree, id, i) + try collect_expression(state, element) + try depend(state, element, id) + } + } + .index: { + expression :: ast.index_expr(tree, id) + try collect_expression(state, expression.base) + try collect_expression(state, expression.index) + try depend(state, expression.base, id) + try depend(state, expression.index, id) + } .invalid: collect_invalid(state, id) } } @hide:file numeric_kind proc(kind TypeKind) bool { - return kind == .int or kind == .float + return kind == .int or kind == .float or kind == .usize } @hide:file @@ -819,23 +892,43 @@ numeric_constraint proc(state @mut State, expression, operand, operation ast.Nod @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 + if (state.types[usize(id)].kind == .invalid) return index :: root(state, usize(id)) + if (expected.kind == .array) { + literal :: state.cells[index].literal + if (literal == ast.NodeId(maxval!(u32))) return + previous :: state.cells[index].expected + if (previous.kind == .array and !same_type(state, previous, expected)) { + if (!has_inferred_count(state, previous) and !has_inferred_count(state, expected)) { + state.cells[index].type = invalid_type(try diagnostics.add(state.diagnostics, code, ast.node_span(state.tree, id))) + return + } + } + state.cells[index].expected = expected + descriptor :: state.arrays.items[usize(expected.array_type)] + len :: ast.array_element_count(state.tree, literal) + if (descriptor.len != no_index and descriptor.len != len) { + try fail(state, literal, .array_length_mismatch, ast.node_span(state.tree, literal)) + } + for (0..len) |i| try demand(state, ast.array_element(state.tree, literal, i), descriptor.element, code) + return + } + if (!numeric_kind(expected.kind)) return 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 -range_bound_context proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { +range_bound_context proc(state @mut State, id ast.NodeId, family TypeKind) void ! (mem.AllocError | diagnostics.CapacityError) { if (state.types[usize(id)].kind == .invalid) return index :: root(state, usize(id)) if (state.cells[index].type.kind == .invalid) return - if (state.cells[index].demand == .float) { + if (state.cells[index].demand != .none and state.cells[index].demand != family) { try fail(state, id, .invalid_range_bound, ast.node_span(state.tree, id)) return } - try demand(state, id, Type{ kind = .int }, .invalid_range_bound) + try demand(state, id, Type{ kind = family }, .invalid_range_bound) } @hide:file @@ -854,6 +947,11 @@ require_bool proc(state @mut State, operand ast.NodeId) void ! (mem.AllocError | @hide:file mutable_target proc(state @State, id ast.NodeId) bool { + if (ast.expr_kind(state.tree, id) == .index) { + view :: ast.index_expr(state.tree, id) + base :: state.types[usize(view.base)] + return base.kind == .array and state.arrays.items[usize(base.array_type)].mutable and mutable_target(state, view.base) + } binding :: sema.binding(state.resolution, id) if (binding.kind != .declaration) return false target :: binding.data.declaration @@ -867,11 +965,11 @@ same_signature proc(state @State, left, right ast.NodeId) bool { if (ast.function_param_count(tree, left) != ast.function_param_count(tree, right)) return false left_result :: ast.function_decl(tree, left).return_type right_result :: ast.function_decl(tree, right).return_type - if (state.types[usize(left_result)].kind != state.types[usize(right_result)].kind) return false + if (!same_type(state, state.types[usize(left_result)], state.types[usize(right_result)])) return false for (0..ast.function_param_count(tree, left)) |i| { left_param :: ast.function_param(tree, left, i) right_param :: ast.function_param(tree, right, i) - if (state.types[usize(left_param)].kind != state.types[usize(right_param)].kind) return false + if (!same_type(state, state.types[usize(left_param)], state.types[usize(right_param)])) return false } return true } @@ -880,9 +978,7 @@ same_signature proc(state @State, left, right ast.NodeId) bool { 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)) - } else if (actual.kind == .function and !same_signature(state, actual.function, expected.function)) { + if (!same_type(state, actual, expected)) { try fail(state, value, .type_mismatch, ast.node_span(state.tree, value)) } } @@ -900,8 +996,14 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost } .assignment_stmt: { assignment :: ast.assignment_stmt(tree, id) + target_type :: state.types[usize(assignment.target)] + if (target_type.kind == .invalid) return if (mutable_target(state, assignment.target)) { - try compatible(state, assignment.value, state.types[usize(assignment.target)]) + try compatible(state, assignment.value, target_type) + } else { + code :: if (ast.expr_kind(tree, assignment.target) == .index) + diagnostics.Code.invalid_index_assignment else diagnostics.Code.invalid_assignment_target + try fail(state, id, code, ast.node_span(tree, assignment.target)) } } .return_stmt: { @@ -912,7 +1014,7 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost actual :: state.types[usize(value)] if (actual.kind == .invalid) { state.types[usize(id)] = actual - } else if (actual.kind != expected.kind) { + } else if (!same_type(state, actual, expected)) { try fail(state, value, .return_type_mismatch, ast.node_span(tree, value)) state.types[usize(id)] = state.types[usize(value)] } @@ -929,7 +1031,7 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost return } operand :: state.types[usize(expression.operand)] - if (operand.kind != .invalid and !numeric_kind(operand.kind)) { + if (operand.kind != .invalid and (!numeric_kind(operand.kind) or operand.kind == .usize)) { try fail(state, id, .invalid_operand, ast.node_span(tree, id)) } } @@ -947,7 +1049,7 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost and left.kind == .bool and right.kind == .bool) 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) { + } else if (expression.operator == .divide and left.kind != .float) { try fail(state, id, .integer_division, expression.span) } } @@ -955,13 +1057,39 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost expression :: ast.range_expr(tree, id) start_type :: state.types[usize(expression.start)] end_type :: state.types[usize(expression.end)] - if (start_type.kind != .invalid and start_type.kind != .int) { + family TypeKind :: if (start_type.kind == .usize or end_type.kind == .usize) .usize else .int + if (start_type.kind != .invalid and start_type.kind != family) { try fail(state, expression.start, .invalid_range_bound, ast.node_span(tree, expression.start)) } - if (end_type.kind != .invalid and end_type.kind != .int) { + if (end_type.kind != .invalid and end_type.kind != family) { try fail(state, expression.end, .invalid_range_bound, ast.node_span(tree, expression.end)) } } + .array_literal: { + actual :: state.types[usize(id)] + if (actual.kind != .array) return + element :: state.arrays.items[usize(actual.array_type)].element + for 0..ast.array_element_count(tree, id) |i| { + child :: ast.array_element(tree, id, i) + if (element.kind == .void or element.kind == .range) { + try fail(state, child, .invalid_array_element_type, ast.node_span(tree, child)) + } else { + try compatible(state, child, element) + } + } + if (array_cells(state, actual) == null) try fail(state, id, .array_size_overflow, ast.node_span(tree, id)) + } + .index: { + view :: ast.index_expr(tree, id) + base :: state.types[usize(view.base)] + index :: state.types[usize(view.index)] + if (index.kind != .invalid and index.kind != .usize) { + try fail(state, view.index, .invalid_index_type, ast.node_span(tree, view.index)) + } + if (base.kind != .invalid and base.kind != .array) { + try fail(state, id, .not_indexable, ast.node_span(tree, view.base)) + } + } .call: try validate_call(state, id) else: {} } @@ -1017,3 +1145,378 @@ block_falls_through proc(state @State, id ast.NodeId) bool { } return true } + +@hide:file +ConstantInteger :: struct { + magnitude u64 + negative bool = false +} + +@hide:file +constant_integer proc(state @mut State, id ast.NodeId) ?ConstantInteger { + if (state.cells[usize(id)].constant_visiting) return null + state.cells[usize(id)].constant_visiting = true + defer state.cells[usize(id)].constant_visiting = false + match ast.expr_kind(state.tree, id) { + .literal_int: { + text :: ast.source_text(state.tree, ast.node_span(state.tree, id)) + value := u64(0) + for text |byte| { + if (byte < '0' or byte > '9') return null + digit :: u64(byte - '0') + if (value > divtrunc!(maxval!(u64) - digit, u64(10))) return null + value = value * 10 + digit + } + return ConstantInteger{ magnitude = value } + } + .identifier: { + binding :: sema.binding(state.resolution, id) + if (binding.kind != .declaration) return null + target :: binding.data.declaration + if (!state.cells[usize(target)].count_declaration) return null + declaration :: ast.statement_decl(state.tree, target) + if (declaration.mutability != .constant) return null + if (declaration.type) |annotation| { + type_binding :: sema.binding(state.resolution, annotation) + if (type_binding.kind != .builtin) return null + kind :: type_binding.data.builtin + if (kind != .int and kind != .i64 and kind != .usize) return null + } + return constant_integer(state, declaration.initializer) + } + .unary: { + view :: ast.unary_expr(state.tree, id) + if (view.operator != .negate) return null + if (constant_integer(state, view.operand)) |value| { + return ConstantInteger{ magnitude = value.magnitude, negative = value.magnitude != 0 and !value.negative } + } + return null + } + .binary: { + view :: ast.binary_expr(state.tree, id) + left := ConstantInteger{ magnitude = 0 } + right := ConstantInteger{ magnitude = 0 } + if (constant_integer(state, view.left)) |value| left = value else return null + if (constant_integer(state, view.right)) |value| right = value else return null + if (view.operator == .multiply) { + if (right.magnitude != 0 and left.magnitude > divtrunc!(maxval!(u64), right.magnitude)) return null + magnitude :: left.magnitude * right.magnitude + return ConstantInteger{ magnitude = magnitude, negative = magnitude != 0 and left.negative != right.negative } + } + if (view.operator != .add and view.operator != .subtract) return null + if (view.operator == .subtract and right.magnitude != 0) right.negative = !right.negative + if (left.negative == right.negative) { + if (left.magnitude > maxval!(u64) - right.magnitude) return null + return ConstantInteger{ magnitude = left.magnitude + right.magnitude, negative = left.negative } + } + if (left.magnitude < right.magnitude) { + return ConstantInteger{ magnitude = right.magnitude - left.magnitude, negative = right.negative } + } + return ConstantInteger{ magnitude = left.magnitude - right.magnitude, negative = left.magnitude != right.magnitude and left.negative } + } + else: return null + } +} + +@hide:file +intern_array proc(state @mut State, descriptor ArrayType) Type ! mem.AllocError { + for state.arrays.items |existing, i| { + if (existing.len == descriptor.len and existing.mutable == descriptor.mutable + and existing.element.kind == descriptor.element.kind + and existing.element.function == descriptor.element.function + and existing.element.array_type == descriptor.element.array_type) { + return Type{ kind = .array, array_type = ArrayId(i) } + } + } + try arraylist.append(&state.arrays, descriptor) + return Type{ kind = .array, array_type = ArrayId(state.arrays.items.len - 1) } +} + +@hide:file +same_type proc(state @State, left, right Type) bool { + if (left.kind != right.kind) return false + if (left.kind == .function) return same_signature(state, left.function, right.function) + if (left.kind != .array) return true + a :: state.arrays.items[usize(left.array_type)] + b :: state.arrays.items[usize(right.array_type)] + return a.len == b.len and a.mutable == b.mutable and same_type(state, a.element, b.element) +} + +@hide:file +array_cells proc(state @State, type Type) ?usize { + if (type.kind != .array) return usize(1) + view :: state.arrays.items[usize(type.array_type)] + if (view.len == no_index) return null + child := usize(0) + if (array_cells(state, view.element)) |value| child = value else return null + if (child != 0 and view.len > divtrunc!(maxval!(usize), child)) return null + return view.len * child +} + +@hide:file +array_origin proc(state @mut State, id ast.NodeId) ast.NodeId { + origin :: state.cells[root(state, usize(id))].literal + if (origin != ast.NodeId(maxval!(u32))) return origin + if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .index) { + base :: array_origin(state, ast.index_expr(state.tree, id).base) + if (base != ast.NodeId(maxval!(u32)) and ast.array_element_count(state.tree, base) != 0) { + return array_origin(state, ast.array_element(state.tree, base, 0)) + } + } + return ast.NodeId(maxval!(u32)) +} + +@hide:file +unify_elements proc(state @mut State, left, right ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + if (state.types[usize(left)].kind == .invalid or state.types[usize(right)].kind == .invalid) return + a :: array_origin(state, left) + b :: array_origin(state, right) + if (a != ast.NodeId(maxval!(u32)) and b != ast.NodeId(maxval!(u32))) { + count :: ast.array_element_count(state.tree, a) + if (count != ast.array_element_count(state.tree, b)) return + for (0..count) |i| try unify_elements(state, ast.array_element(state.tree, a, i), ast.array_element(state.tree, b, i)) + return + } + left_type :: group_type(state, left) + right_type :: group_type(state, right) + if (numeric_kind(left_type.kind)) try demand(state, right, left_type, .type_mismatch) + if (numeric_kind(right_type.kind)) try demand(state, left, right_type, .type_mismatch) + left_root :: root(state, usize(left)) + right_root :: root(state, usize(right)) + if (state.cells[left_root].numeric and state.cells[right_root].numeric) { + try numeric_constraint(state, left, right, right) + } + if (left_type.kind == .array) try demand(state, right, left_type, .type_mismatch) + if (right_type.kind == .array) try demand(state, left, right_type, .type_mismatch) +} + +@hide:file +infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.AllocError | diagnostics.CapacityError) { + index :: root(state, usize(id)) + if (state.cells[index].visiting) return state.cells[index].type + if (state.cells[index].type.kind == .invalid) return state.cells[index].type + state.cells[index].visiting = true + defer state.cells[index].visiting = false + if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .identifier) { + binding :: sema.binding(state.resolution, id) + if (binding.kind == .declaration) { + target :: binding.data.declaration + if (state.cells[usize(target)].role == .declaration and state.cells[usize(target)].fixed + and has_inferred_count(state, state.cells[usize(target)].type)) { + declaration :: ast.statement_decl(state.tree, target) + actual :: try infer_shape(state, declaration.initializer, defaults) + if (actual.kind == .array and !has_inferred_count(state, actual)) { + try specialize_annotation(state, declaration.type?, actual) + state.cells[usize(target)].type = state.types[usize(declaration.type?)] + seed(state, id, state.cells[usize(target)].type) + } + } + } + } + literal :: state.cells[index].literal + if (literal != ast.NodeId(maxval!(u32))) { + len :: ast.array_element_count(state.tree, literal) + expected :: state.cells[index].expected + element := Type{} + mutable := false + if (expected.kind == .array) { + view :: state.arrays.items[usize(expected.array_type)] + element = view.element + mutable = view.mutable + } + for 0..len |i| { + child :: ast.array_element(state.tree, literal, i) + actual :: try infer_shape(state, child, defaults) + if (element.kind == .none and actual.kind != .none and actual.kind != .invalid) element = actual + if (element.kind == .array and actual.kind == .array) element = try specialize_type(state, element, actual) + if (element.kind != .none) try demand(state, child, element, .type_mismatch) + } + if (element.kind == .none and len == 0 and defaults) element = Type{ kind = .int } + if (element.kind != .none) { + type :: try intern_array(state, ArrayType{ element = element, len = len, mutable = mutable }) + state.cells[index].type = type + return type + } + return Type{} + } + if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .index) { + view :: ast.index_expr(state.tree, id) + base :: try infer_shape(state, view.base, defaults) + if (base.kind == .array) { + element :: state.arrays.items[usize(base.array_type)].element + seed(state, id, element) + return element + } + origin :: array_origin(state, view.base) + if (origin != ast.NodeId(maxval!(u32)) and ast.array_element_count(state.tree, origin) != 0) { + child :: ast.array_element(state.tree, origin, 0) + child_root :: root(state, usize(child)) + if (state.cells[child_root].numeric) { + try numeric_constraint(state, id, child, id) + } + } + } + return group_type(state, id) +} + +@hide:file +prepare_shapes proc(state @mut State, assignment_values, defaults bool) void ! (mem.AllocError | diagnostics.CapacityError) { + for 0..(state.cells.len) |i| { + if (state.cells[i].role != .expression or state.cells[i].assignment_value != assignment_values) continue + id :: ast.NodeId(i) + if ( + ast.expr_kind(state.tree, id) == .array_literal + and state.cells[root(state, i)].expected.kind != .array + ) { + len :: ast.array_element_count(state.tree, id) + for (usize(1)..len) |j| try unify_elements(state, ast.array_element(state.tree, id, 0), ast.array_element(state.tree, id, j)) + } + _ = try infer_shape(state, id, defaults) + } +} + +@hide:file +apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem.AllocError | diagnostics.CapacityError) { + tree :: state.tree + for 0..(state.cells.len) |i| { + id :: ast.NodeId(i) + if (!assignment_values and state.cells[i].role == .declaration) { + view :: ast.statement_decl(tree, id) + if (view.type) |annotation| try context_demand(state, view.initializer, state.types[usize(annotation)], .type_mismatch, early) + } + if (!assignment_values and state.cells[i].role == .return_stmt) { + if (ast.return_stmt(tree, id).value) |value| { + try context_demand(state, value, state.types[usize(state.cells[i].return_annotation)], .return_type_mismatch, early) + } + } + if (assignment_values and state.cells[i].role == .assignment_stmt) { + view :: ast.assignment_stmt(tree, id) + if (mutable_target(state, view.target)) try context_demand(state, view.value, state.types[usize(view.target)], .type_mismatch, early) + } + if (state.cells[i].role != .expression or state.cells[i].assignment_value != assignment_values 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 + count :: ast.function_param_count(tree, signature.function) + for 0..ast.call_arg_count(tree, id) |j| { + if (j >= count) continue + parameter :: ast.function_param(tree, signature.function, j) + try context_demand(state, ast.call_arg(tree, id, j), state.cells[usize(parameter)].type, .type_mismatch, early) + } + } +} + +@hide:file +context_demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code, early bool) void ! (mem.AllocError | diagnostics.CapacityError) { + if (early and expected.kind != .array and expected.kind != .usize) return + try demand(state, id, expected, code) +} + +@hide:file +bound_family proc(state @mut State, id ast.NodeId) TypeKind { + index :: root(state, usize(id)) + if (state.cells[index].type.kind != .none) return state.cells[index].type.kind + if (state.cells[index].demand != .none) return state.cells[index].demand + producer :: state.cells[index].producer + if (producer == ast.NodeId(maxval!(u32)) or state.cells[index].visiting) return .none + state.cells[index].visiting = true + defer state.cells[index].visiting = false + if (ast.expr_kind(state.tree, producer) == .unary) return bound_family(state, ast.unary_expr(state.tree, producer).operand) + view :: ast.binary_expr(state.tree, producer) + left :: bound_family(state, view.left) + right :: bound_family(state, view.right) + if (left == .float or right == .float) return .float + if (left == .usize or right == .usize) return .usize + if (left == .int or right == .int) return .int + return .none +} + +@hide:file +prepare_ranges proc(state @mut State, assignment_values, constrain bool) void ! (mem.AllocError | diagnostics.CapacityError) { + tree :: state.tree + for 0..(state.cells.len) |i| { + id :: ast.NodeId(i) + if (state.cells[i].role != .expression or state.cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .range) continue + view :: ast.range_expr(tree, id) + left :: bound_family(state, view.start) + right :: bound_family(state, view.end) + family TypeKind :: if (left == .usize or right == .usize) .usize else .int + if (constrain) { + try range_bound_context(state, view.start, family) + try range_bound_context(state, view.end, family) + } + if (group_type(state, id).kind != .invalid) { + seed(state, id, Type{ kind = .range, range_element = family }) + state.types[i] = group_type(state, id) + } + capture :: state.cells[i].capture + if (capture != ast.NodeId(maxval!(u32)) and state.cells[usize(capture)].type.kind != .invalid) { + seed(state, capture, Type{ kind = family }) + } + } + for 0..(state.cells.len) |i| { + id :: ast.NodeId(i) + if (state.cells[i].role != .expression or state.cells[i].assignment_value or ast.expr_kind(tree, id) != .identifier) continue + binding :: sema.binding(state.resolution, id) + if (binding.kind == .declaration and state.cells[usize(binding.data.declaration)].role == .loop_capture) { + seed(state, id, group_type(state, binding.data.declaration)) + } + } +} + +@hide:file +has_inferred_count proc(state @State, type Type) bool { + if (type.kind != .array) return false + view :: state.arrays.items[usize(type.array_type)] + return view.len == no_index or has_inferred_count(state, view.element) +} + +@hide:file +specialize_type proc(state @mut State, expected, actual Type) Type ! mem.AllocError { + if (expected.kind != .array or actual.kind != .array) return expected + view := state.arrays.items[usize(expected.array_type)] + source :: state.arrays.items[usize(actual.array_type)] + if (view.len == no_index) view.len = source.len + view.element = try specialize_type(state, view.element, source.element) + return try intern_array(state, view) +} + +@hide:file +specialize_annotation proc(state @mut State, id ast.NodeId, actual Type) void ! mem.AllocError { + expected :: try specialize_type(state, state.types[usize(id)], actual) + state.types[usize(id)] = expected + state.cells[usize(id)].type = expected + if (expected.kind == .array and ast.expr_kind(state.tree, id) == .array_type) { + try specialize_annotation(state, ast.array_type(state.tree, id).element, state.arrays.items[usize(expected.array_type)].element) + } +} + +@hide:file +specialize_annotations proc(state @mut State) void ! (mem.AllocError | diagnostics.CapacityError) { + for 0..(state.cells.len) |i| { + if (state.cells[i].role != .declaration) continue + view :: ast.statement_decl(state.tree, ast.NodeId(i)) + if (view.type) |annotation| { + actual :: try infer_shape(state, view.initializer, true) + try specialize_annotation(state, annotation, actual) + if (state.cells[i].type.kind != .invalid) state.cells[i].type = state.types[usize(annotation)] + } + } + for 0..(state.cells.len) |i| { + if (state.cells[i].role != .annotation or state.types[i].kind != .array) continue + if (has_inferred_count(state, state.types[i])) { + try fail(state, ast.NodeId(i), .invalid_array_length, ast.node_span(state.tree, ast.NodeId(i))) + state.cells[i].type = state.types[i] + } else if (array_cells(state, state.types[i]) == null) { + try fail(state, ast.NodeId(i), .array_size_overflow, ast.node_span(state.tree, ast.NodeId(i))) + state.cells[i].type = state.types[i] + } + } + for 0..(state.cells.len) |i| { + id :: ast.NodeId(i) + if (state.cells[i].role != .expression or state.cells[i].assignment_value or ast.expr_kind(state.tree, id) != .identifier) continue + binding :: sema.binding(state.resolution, id) + if (binding.kind == .declaration and state.cells[usize(binding.data.declaration)].fixed) { + seed(state, id, state.cells[usize(binding.data.declaration)].type) + } + } +} diff --git a/source/sema/check/check.test.hon b/source/sema/check/check.test.hon index 177c88f..a551401 100644 --- a/source/sema/check/check.test.hon +++ b/source/sema/check/check.test.hon @@ -2129,3 +2129,474 @@ invalid_range_capture_bindings_keep_resolver_diagnostic_ids_without_erasing_head try expect_kind(&result, ast.block_statement(&tree, sibling_loop.body, 0), .int) try testing.expect_equal(previous_count, diagnostics.count(&diagnostic_store)) } + +@hide:file +ArrayFixture :: struct { + tree ast.Tree + resolution sema.Resolution + diagnostics diagnostics.Store + result Result +} + +@hide:file +array_fixture proc(source []u8) ArrayFixture ! (mem.AllocError | strpool.InternError | diagnostics.CapacityError | lexer.SourceError) { + store := diagnostics.init(mem.c_allocator) + errdefer diagnostics.deinit(&store) + scan := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan) + try lexer.scan(&scan, source, &store) + tree := try parser.parse(mem.c_allocator, scan.tokens.items, source, &store) + errdefer ast.deinit(&tree) + resolution := try sema.resolve(mem.c_allocator, &tree, &store) + errdefer sema.deinit(&resolution) + result :: try check(mem.c_allocator, &tree, &resolution, &store) + return ArrayFixture{ tree = tree, resolution = resolution, diagnostics = store, result = result } +} + +@hide:file +deinit_array_fixture proc(fixture @mut ArrayFixture) void { + deinit(&fixture.result) + sema.deinit(&fixture.resolution) + ast.deinit(&fixture.tree) + diagnostics.deinit(&fixture.diagnostics) +} + +@hide:file +expect_array proc(result @Result, id ast.NodeId, len usize, mutable bool, element TypeKind) ArrayType ! testing.Error { + try expect_kind(result, id, .array) + actual :: array_type(result, type_of(result, id).array_type) + try testing.expect_equal(len, actual.len) + try testing.expect_equal(mutable, actual.mutable) + try testing.expect_equal(element, actual.element.kind) + return actual +} + +fixed_arrays_contextualize_forward_aliases_counts_nested_literals_and_signatures test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + source :: + `context [2]f64 :: alias + `alias :: seed + `seed := [1, 2] + `count :: later * 2 - 1 + `later :: 2 + `nested [_]mut [2]mut i64 := [[1, 2], [3, 4]] + `empty [_]bool :: [] + `plain :: [] + `copy :: nested + `index usize :: 0 + `child float :: seed[index] + `produce proc(value [2]f64) [2]f64 { return value } + `called :: produce([5, 6]) + `from_result [_]f64 :: called + `counted [count]i64 :: [1, 2, 3] + fixture := try array_fixture(source) + defer deinit_array_fixture(&fixture) + tree :: &fixture.tree + result :: &fixture.result + for (0..usize(3)) |i| _ = try expect_array(result, ast.package_decl(tree, i), 2, false, .float) + seed :: ast.statement_decl(tree, ast.package_decl(tree, 2)).initializer + for (0..ast.array_element_count(tree, seed)) |i| try expect_kind(result, ast.array_element(tree, seed, i), .float) + nested :: ast.package_decl(tree, 5) + shape :: try expect_array(result, nested, 2, true, .array) + row :: array_type(result, shape.element.array_type) + try testing.expect_equal(2, row.len) + try testing.expect(row.mutable) + try testing.expect_equal(TypeKind.int, row.element.kind) + annotation :: ast.statement_decl(tree, nested).type? + _ = try expect_array(result, annotation, 2, true, .array) + _ = try expect_array(result, ast.package_decl(tree, 6), 0, false, .bool) + _ = try expect_array(result, ast.package_decl(tree, 7), 0, false, .int) + try testing.expect_equal(type_of(result, nested).array_type, type_of(result, ast.package_decl(tree, 8)).array_type) + indexed :: ast.index_expr(tree, ast.statement_decl(tree, ast.package_decl(tree, 10)).initializer) + try expect_kind(result, indexed.index, .usize) + try expect_kind(result, ast.statement_decl(tree, ast.package_decl(tree, 10)).initializer, .float) + function :: ast.package_decl(tree, 11) + _ = try expect_array(result, ast.function_param(tree, function, 0), 2, false, .float) + _ = try expect_array(result, ast.function_decl(tree, function).return_type, 2, false, .float) + call :: ast.statement_decl(tree, ast.package_decl(tree, 12)).initializer + argument :: ast.call_arg(tree, call, 0) + _ = try expect_array(result, argument, 2, false, .float) + try expect_kind(result, ast.array_element(tree, argument, 0), .float) + _ = try expect_array(result, ast.package_decl(tree, 13), 2, false, .float) + _ = try expect_array(result, ast.package_decl(tree, 14), 3, false, .int) + try testing.expect(array_type_count(result) > 0) + try testing.expect_equal(0, diagnostics.count(&fixture.diagnostics)) +} + +nested_array_numeric_context_flows_through_index_children_before_defaults test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + source :: + `first float :: later[0][0] + `later := [[1, 2], [3, 4]] + `row :: later[0] + `mixed :: [[1, 2], [3.5, 4]] + `identity proc(value int) int { return value } + `another proc(value int) int { return value + 1 } + `functions :: [identity, another] + fixture := try array_fixture(source) + defer deinit_array_fixture(&fixture) + tree :: &fixture.tree + result :: &fixture.result + for usize(1)..usize(4) |i| { + element_kind TypeKind :: if (i == 2) .float else .array + type :: try expect_array(result, ast.package_decl(tree, i), 2, false, element_kind) + if (type.element.kind == .array) { + try testing.expect_equal(TypeKind.float, array_type(result, type.element.array_type).element.kind) + } + } + matrix :: ast.statement_decl(tree, ast.package_decl(tree, 1)).initializer + for 0..ast.array_element_count(tree, matrix) |i| { + row :: ast.array_element(tree, matrix, i) + for (0..ast.array_element_count(tree, row)) |j| try expect_kind(result, ast.array_element(tree, row, j), .float) + } + outer :: ast.statement_decl(tree, ast.package_decl(tree, 0)).initializer + inner :: ast.index_expr(tree, outer).base + _ = try expect_array(result, inner, 2, false, .float) + try expect_kind(result, outer, .float) + try expect_kind(result, ast.index_expr(tree, outer).index, .usize) + try expect_kind(result, ast.index_expr(tree, inner).index, .usize) + _ = try expect_array(result, ast.package_decl(tree, 6), 2, false, .function) + try testing.expect_equal(0, diagnostics.count(&fixture.diagnostics)) +} + +fixed_index_errors_are_occurrence_local_and_preserve_sources_and_siblings test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + source :: + `fixed i64 :: 0 + `inferred := 0 + `source [2]i64 :: [10, 20] + `bad :: source[fixed] + `good :: source[inferred] + `boolean :: source[true] + `missing :: source[unknown] + `tail :: source[1] + fixture := try array_fixture(source) + defer deinit_array_fixture(&fixture) + tree :: &fixture.tree + result :: &fixture.result + try expect_kind(result, ast.package_decl(tree, 0), .int) + try expect_kind(result, ast.package_decl(tree, 1), .usize) + _ = try expect_array(result, ast.package_decl(tree, 2), 2, false, .int) + bad_id :: ast.statement_decl(tree, ast.package_decl(tree, 3)).initializer + bad :: ast.index_expr(tree, bad_id) + original :: try expect_error(result, &fixture.diagnostics, bad.index, .invalid_index_type, ast.node_span(tree, bad.index)) + try testing.expect_equal(original, type_of(result, bad_id).diagnostic) + try testing.expect_equal(original, type_of(result, ast.package_decl(tree, 3)).diagnostic) + _ = try expect_array(result, bad.base, 2, false, .int) + try expect_kind(result, ast.package_decl(tree, 4), .int) + boolean :: ast.index_expr(tree, ast.statement_decl(tree, ast.package_decl(tree, 5)).initializer).index + _ = try expect_error(result, &fixture.diagnostics, boolean, .invalid_index_type, ast.node_span(tree, boolean)) + missing :: ast.index_expr(tree, ast.statement_decl(tree, ast.package_decl(tree, 6)).initializer).index + try testing.expect_equal(sema.binding(&fixture.resolution, missing).data.diagnostic, type_of(result, missing).diagnostic) + try expect_kind(result, ast.package_decl(tree, 7), .int) + try testing.expect_equal(2, code_count(&fixture.diagnostics, .invalid_index_type)) + try testing.expect_equal(1, code_count(&fixture.diagnostics, .unknown_name)) + try testing.expect_equal(3, diagnostics.count(&fixture.diagnostics)) +} + +array_lengths_require_supported_constexpr_and_finalize_nested_inferred_counts test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + source :: + `two :: tail + `tail :: 2 + `valid [two * 3 - 4]i64 :: [1, 2] + `zero [3 - 3]i64 :: [] + `nested [_]mut [_]mut i64 := [[1, 2], [3, 4]] + `negative [-1]i64 :: [] + `fraction [1.5]i64 :: [] + `runtime_count := 2 + `runtime_array [runtime_count]i64 :: [1, 2] + `count proc() int { return 2 } + `called [count()]i64 :: [1, 2] + `a :: b + `b :: a + `cycle [a]i64 :: [] + `overflow [18446744073709551615 + 1]i64 :: [] + `wrong [2]i64 :: [1] + `constraints [2]mut int := [1, 2] + `floats [0]float :: [] + `voids [0]void :: [] + `missing [absent]i64 :: [] + fixture := try array_fixture(source) + defer deinit_array_fixture(&fixture) + tree :: &fixture.tree + result :: &fixture.result + _ = try expect_array(result, ast.package_decl(tree, 2), 2, false, .int) + _ = try expect_array(result, ast.package_decl(tree, 3), 0, false, .int) + nested :: ast.statement_decl(tree, ast.package_decl(tree, 4)) + shape :: try expect_array(result, nested.type?, 2, true, .array) + child :: array_type(result, shape.element.array_type) + try testing.expect_equal(2, child.len) + try testing.expect(child.mutable) + _ = try expect_array(result, ast.array_type(tree, nested.type?).element, 2, true, .int) + invalid_lengths [6]usize :: [5, 6, 8, 10, 13, 14] + for invalid_lengths |i| { + annotation :: ast.statement_decl(tree, ast.package_decl(tree, i)).type? + count :: ast.array_type(tree, annotation).count? + _ = try expect_error(result, &fixture.diagnostics, annotation, .invalid_array_length, ast.node_span(tree, count)) + } + wrong :: ast.statement_decl(tree, ast.package_decl(tree, 15)).initializer + _ = try expect_error(result, &fixture.diagnostics, wrong, .array_length_mismatch, ast.node_span(tree, wrong)) + for usize(16)..usize(19) |i| { + annotation :: ast.statement_decl(tree, ast.package_decl(tree, i)).type? + child_id :: ast.array_type(tree, annotation).element + _ = try expect_error(result, &fixture.diagnostics, annotation, .invalid_array_element_type, ast.node_span(tree, child_id)) + } + missing :: ast.statement_decl(tree, ast.package_decl(tree, 19)).type? + count :: ast.array_type(tree, missing).count? + try testing.expect_equal(sema.binding(&fixture.resolution, count).data.diagnostic, type_of(result, missing).diagnostic) + try testing.expect_equal(6, code_count(&fixture.diagnostics, .invalid_array_length)) + try testing.expect_equal(1, code_count(&fixture.diagnostics, .array_length_mismatch)) + try testing.expect_equal(3, code_count(&fixture.diagnostics, .invalid_array_element_type)) +} + +indexed_writes_require_mutable_local_root_and_each_array_level_but_copies_are_values test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + source :: + `global [1]mut i64 := [1] + `main proc(parameter [1]mut i64) void { + ` mutable [1]mut i64 := [1] + ` mutable[0] = 2 + ` copied := mutable + ` copied[0] = 3 + ` inferred := [1] + ` inferred[0] = 4 + ` inferred = [5] + ` constant [1]mut i64 :: [1] + ` constant[0] = 6 + ` parameter[0] = 7 + ` global[0] = 8 + ` outer_readonly [1][1]mut i64 := [[1]] + ` (outer_readonly[0])[0] = 9 + ` inner_readonly [1]mut [1]i64 := [[1]] + ` (inner_readonly[0])[0] = 10 + ` both [1]mut [1]mut i64 := [[1]] + ` (both[0])[0] = 11 + ` both[0] = [12] + ` mismatch [1]mut i64 := inner_readonly[0] + `} + fixture := try array_fixture(source) + defer deinit_array_fixture(&fixture) + tree :: &fixture.tree + result :: &fixture.result + body :: ast.function_decl(tree, ast.package_decl(tree, 1)).body + valid [6]usize :: [1, 3, 6, 16, 17, 2] + for valid |i| { + id :: ast.block_statement(tree, body, i) + kind TypeKind :: if (i == 2) .array else .void + try expect_kind(result, id, kind) + } + invalid [6]usize :: [5, 8, 9, 10, 12, 14] + for invalid |i| { + id :: ast.block_statement(tree, body, i) + target :: ast.assignment_stmt(tree, id).target + _ = try expect_error(result, &fixture.diagnostics, id, .invalid_index_assignment, ast.node_span(tree, target)) + try expect_kind(result, target, .int) + try expect_kind(result, ast.index_expr(tree, target).index, .usize) + } + mismatch :: ast.statement_decl(tree, ast.block_statement(tree, body, 18)).initializer + _ = try expect_error(result, &fixture.diagnostics, mismatch, .type_mismatch, ast.node_span(tree, mismatch)) + _ = try expect_array(result, ast.block_statement(tree, body, 4), 1, false, .int) + _ = try expect_array(result, ast.function_param(tree, ast.package_decl(tree, 1), 0), 1, true, .int) + try testing.expect_equal(6, code_count(&fixture.diagnostics, .invalid_index_assignment)) + try testing.expect_equal(1, code_count(&fixture.diagnostics, .type_mismatch)) +} + +usize_arithmetic_ranges_and_readonly_captures_keep_the_bound_family test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + source :: + `limit usize :: 18446744073709551615 + `arithmetic usize :: (limit - 2) * 1 + 1 + `ordered :: 0 < limit + `equal :: limit == 18446744073709551615 + `negated :: -limit + `divided :: limit / 2 + `fixed i64 :: 1 + `mixed :: fixed + limit + `main proc() void { + ` source [2]i64 :: [1, 2] + ` for 0..limit |index| { + ` source[index] + ` index = 1 + ` } + ` for 0..2 |index| { source[index] } + ` for limit..=limit |_| {} + `} + fixture := try array_fixture(source) + defer deinit_array_fixture(&fixture) + tree :: &fixture.tree + result :: &fixture.result + try expect_kind(result, ast.package_decl(tree, 0), .usize) + try expect_kind(result, ast.package_decl(tree, 1), .usize) + try expect_kind(result, ast.package_decl(tree, 2), .bool) + try expect_kind(result, ast.package_decl(tree, 3), .bool) + for usize(4)..usize(8) |i| { + if (i == 6) continue + value :: ast.statement_decl(tree, ast.package_decl(tree, i)).initializer + code diagnostics.Code :: if (i == 5) .integer_division else .invalid_operand + _ = try expect_error(result, &fixture.diagnostics, value, code, ast.node_span(tree, value)) + } + body :: ast.function_decl(tree, ast.package_decl(tree, 8)).body + unsigned_loop :: ast.for_stmt(tree, ast.block_statement(tree, body, 1)) + try expect_kind(result, unsigned_loop.capture?, .usize) + try testing.expect_equal(TypeKind.usize, type_of(result, unsigned_loop.iterable).range_element) + interval :: ast.range_expr(tree, unsigned_loop.iterable) + try expect_kind(result, interval.start, .usize) + try expect_kind(result, interval.end, .usize) + read :: ast.statement_expr(tree, ast.block_statement(tree, unsigned_loop.body, 0)) + try expect_kind(result, read, .int) + try expect_kind(result, ast.index_expr(tree, read).index, .usize) + readonly :: ast.block_statement(tree, unsigned_loop.body, 1) + target :: ast.assignment_stmt(tree, readonly).target + _ = try expect_error(result, &fixture.diagnostics, readonly, .invalid_assignment_target, ast.node_span(tree, target)) + try expect_kind(result, target, .usize) + signed_loop :: ast.for_stmt(tree, ast.block_statement(tree, body, 2)) + try expect_kind(result, signed_loop.capture?, .int) + signed_read :: ast.statement_expr(tree, ast.block_statement(tree, signed_loop.body, 0)) + bad_index :: ast.index_expr(tree, signed_read).index + _ = try expect_error(result, &fixture.diagnostics, bad_index, .invalid_index_type, ast.node_span(tree, bad_index)) + try testing.expect_equal(TypeKind.int, type_of(result, signed_loop.iterable).range_element) + _ = try expect_array(result, ast.block_statement(tree, body, 0), 2, false, .int) + ignored :: ast.for_stmt(tree, ast.block_statement(tree, body, 3)) + try testing.expect_equal(TypeKind.usize, type_of(result, ignored.iterable).range_element) + try testing.expect_equal(1, code_count(&fixture.diagnostics, .invalid_index_type)) + try testing.expect_equal(0, code_count(&fixture.diagnostics, .invalid_range_bound)) +} + +array_assignment_rhs_is_contextual_but_does_not_retarget_established_sources test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + source :: + `main proc() void { + ` target [2]mut f64 := [1, 2] + ` seed := 3 + ` source [2]i64 :: [4, 5] + ` target = [seed, 6] + ` target = source + ` target = [1] + ` matrix [1]mut [2]mut i64 := [[1, 2]] + ` matrix[0] = [7, 8] + ` matrix[0] = [true, false] + ` unknown [1]mut i64 := [absent] + ` unknown[0] = 9 + ` good := 10 + `} + fixture := try array_fixture(source) + defer deinit_array_fixture(&fixture) + tree :: &fixture.tree + result :: &fixture.result + body :: ast.function_decl(tree, ast.package_decl(tree, 0)).body + _ = try expect_array(result, ast.block_statement(tree, body, 0), 2, true, .float) + try expect_kind(result, ast.block_statement(tree, body, 1), .int) + _ = try expect_array(result, ast.block_statement(tree, body, 2), 2, false, .int) + contextual :: ast.assignment_stmt(tree, ast.block_statement(tree, body, 3)).value + _ = try expect_error(result, &fixture.diagnostics, ast.array_element(tree, contextual, 0), .type_mismatch, ast.node_span(tree, ast.array_element(tree, contextual, 0))) + try expect_kind(result, ast.array_element(tree, contextual, 1), .float) + copied :: ast.assignment_stmt(tree, ast.block_statement(tree, body, 4)).value + _ = try expect_error(result, &fixture.diagnostics, copied, .type_mismatch, ast.node_span(tree, copied)) + wrong_len :: ast.assignment_stmt(tree, ast.block_statement(tree, body, 5)).value + _ = try expect_error(result, &fixture.diagnostics, wrong_len, .array_length_mismatch, ast.node_span(tree, wrong_len)) + try expect_kind(result, ast.block_statement(tree, body, 7), .void) + literal :: ast.statement_decl(tree, ast.block_statement(tree, body, 9)).initializer + missing :: ast.array_element(tree, literal, 0) + original :: sema.binding(&fixture.resolution, missing).data.diagnostic + try testing.expect_equal(original, type_of(result, literal).diagnostic) + try testing.expect_equal(original, type_of(result, ast.block_statement(tree, body, 9)).diagnostic) + try testing.expect_equal(original, type_of(result, ast.block_statement(tree, body, 10)).diagnostic) + try expect_kind(result, ast.block_statement(tree, body, 11), .int) +} + +array_signature_identity_includes_recursive_counts_and_mutability test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + source :: + `accept proc(value [1]mut i64) [1]mut i64 { return value } + `different_count proc(value [2]mut i64) [1]mut i64 { return [1] } + `different_mutability proc(value [1]i64) [1]mut i64 { return [1] } + `functions :: [accept, different_count, different_mutability] + `wrong_return proc() [1]mut i64 { + ` value [1]i64 :: [1] + ` return value + `} + `take proc(value [2]i64) void {} + `caller proc() void { + ` take([1]) + ` take([true, false]) + `} + fixture := try array_fixture(source) + defer deinit_array_fixture(&fixture) + tree :: &fixture.tree + result :: &fixture.result + functions :: ast.statement_decl(tree, ast.package_decl(tree, 3)).initializer + for usize(1)..usize(3) |i| { + value :: ast.array_element(tree, functions, i) + _ = try expect_error(result, &fixture.diagnostics, value, .type_mismatch, ast.node_span(tree, value)) + try expect_kind(result, ast.package_decl(tree, i), .function) + } + wrong :: ast.package_decl(tree, 4) + wrong_body :: ast.function_decl(tree, wrong).body + value :: ast.return_stmt(tree, ast.block_statement(tree, wrong_body, 1)).value? + _ = try expect_error(result, &fixture.diagnostics, value, .return_type_mismatch, ast.node_span(tree, value)) + _ = try expect_array(result, ast.block_statement(tree, wrong_body, 0), 1, false, .int) + try expect_kind(result, wrong, .function) + caller :: ast.function_decl(tree, ast.package_decl(tree, 6)).body + call :: ast.statement_expr(tree, ast.block_statement(tree, caller, 0)) + argument :: ast.call_arg(tree, call, 0) + _ = try expect_error(result, &fixture.diagnostics, argument, .array_length_mismatch, ast.node_span(tree, argument)) + second :: ast.statement_expr(tree, ast.block_statement(tree, caller, 1)) + literal :: ast.call_arg(tree, second, 0) + for 0..ast.array_element_count(tree, literal) |i| { + child :: ast.array_element(tree, literal, i) + _ = try expect_error(result, &fixture.diagnostics, child, .type_mismatch, ast.node_span(tree, child)) + } + try expect_kind(result, ast.package_decl(tree, 5), .function) +} + +inferred_counts_follow_forward_fixed_aliases_empty_shapes_and_reject_local_counts test { + strpool.strings = strpool.init(mem.c_allocator) + defer strpool.deinit(&strpool.strings) + source :: + `forward [_]i64 :: later + `later [_]i64 :: [1, 2] + `zero [_]mut bool := [] + `nested [_][_]bool :: [[], []] + `unsigned usize :: 5 + `main proc() void { + ` local_count :: 2 + ` unsupported [local_count]i64 :: [1, 2] + ` values [2]i64 :: [1, 2] + ` for 0..(unsigned + 1) |index| { values[index] } + `} + fixture := try array_fixture(source) + defer deinit_array_fixture(&fixture) + tree :: &fixture.tree + result :: &fixture.result + for 0..usize(2) |i| { + declaration :: ast.statement_decl(tree, ast.package_decl(tree, i)) + _ = try expect_array(result, declaration.type?, 2, false, .int) + _ = try expect_array(result, ast.package_decl(tree, i), 2, false, .int) + } + _ = try expect_array(result, ast.package_decl(tree, 2), 0, true, .bool) + shape :: try expect_array(result, ast.package_decl(tree, 3), 2, false, .array) + child :: array_type(result, shape.element.array_type) + try testing.expect_equal(0, child.len) + try testing.expect_equal(TypeKind.bool, child.element.kind) + body :: ast.function_decl(tree, ast.package_decl(tree, 5)).body + unsupported :: ast.statement_decl(tree, ast.block_statement(tree, body, 1)).type? + count :: ast.array_type(tree, unsupported).count? + _ = try expect_error(result, &fixture.diagnostics, unsupported, .invalid_array_length, ast.node_span(tree, count)) + try expect_kind(result, ast.block_statement(tree, body, 0), .int) + loop :: ast.for_stmt(tree, ast.block_statement(tree, body, 3)) + try expect_kind(result, loop.capture?, .usize) + try testing.expect_equal(TypeKind.usize, type_of(result, loop.iterable).range_element) + read :: ast.statement_expr(tree, ast.block_statement(tree, loop.body, 0)) + try expect_kind(result, read, .int) + try testing.expect_equal(1, code_count(&fixture.diagnostics, .invalid_array_length)) + try testing.expect_equal(1, diagnostics.count(&fixture.diagnostics)) +} diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index 3b97264..d994d12 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -6,7 +6,12 @@ import "@std/strmap" import "@source/ast" import "@source/diagnostics" -BuiltinType :: enum { int, float, bool, string, void } +BuiltinType :: enum { + int, float, + bool, string, + void, i64, + f64, usize, +} @hide:file builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([ @@ -15,6 +20,9 @@ builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([ { "bool", .bool }, { "string", .string }, { "void", .void }, + { "i64", .i64 }, + { "f64", .f64 }, + { "usize", .usize }, ]) BindingKind :: enum { none, declaration, builtin, invalid } @@ -299,6 +307,21 @@ resolve_expr proc( try resolve_expr(state, scope, ast.call_arg(tree, id, i)) } } + .array_type: { + type :: ast.array_type(tree, id) + if (type.count) |count| try resolve_expr(state, scope, count) + try resolve_expr(state, scope, type.element) + } + .array_literal: { + for (0..ast.array_element_count(tree, id)) |i| { + try resolve_expr(state, scope, ast.array_element(tree, id, i)) + } + } + .index: { + expression :: ast.index_expr(tree, id) + try resolve_expr(state, scope, expression.base) + try resolve_expr(state, scope, expression.index) + } .invalid: resolve_invalid(state, id) } } diff --git a/source/sema/resolve.test.hon b/source/sema/resolve.test.hon index c83ba2c..92e0492 100644 --- a/source/sema/resolve.test.hon +++ b/source/sema/resolve.test.hon @@ -603,3 +603,155 @@ range_captures_preserve_duplicate_builtin_and_discard_errors test { try testing.expect_equal(BindingKind.invalid, binding(&resolution, bounds.end).kind) try testing.expect_equal(8, diagnostics.count(&diagnostic_store)) } + +resolves_array_counts_elements_indices_and_unreachable_targets 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 :: + `size usize :: 2 + `make proc(first, second [size]mut [2]i64) [size]i64 { + ` return first[0] + ` dead [local_size]mut [size - 1]f64 := [[later]] + ` local_size :: size + ` later f64 :: 1.0 + ` (dead[index])[0] = later + ` index usize :: 0 + `} + 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 resolve(mem.c_allocator, &tree, &diagnostic_store) + defer deinit(&resolution) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + size :: ast.package_decl(&tree, 0) + function :: ast.package_decl(&tree, 1) + view :: ast.function_decl(&tree, function) + return_type :: ast.array_type(&tree, view.return_type) + try testing.expect_equal(size, binding(&resolution, return_type.count?).data.declaration) + try testing.expect_equal(BuiltinType.i64, binding(&resolution, return_type.element).data.builtin) + first :: ast.function_param(&tree, function, 0) + second :: ast.function_param(&tree, function, 1) + first_type :: ast.param(&tree, first).type + try testing.expect_equal(first_type, ast.param(&tree, second).type) + parameter_array :: ast.array_type(&tree, first_type) + try testing.expect_equal(size, binding(&resolution, parameter_array.count?).data.declaration) + row :: ast.array_type(&tree, parameter_array.element) + try testing.expect_equal(BuiltinType.i64, binding(&resolution, row.element).data.builtin) + returned :: ast.return_stmt(&tree, ast.block_statement(&tree, view.body, 0)).value? + try testing.expect_equal(first, binding(&resolution, ast.index_expr(&tree, returned).base).data.declaration) + dead :: ast.block_statement(&tree, view.body, 1) + local_size :: ast.block_statement(&tree, view.body, 2) + later :: ast.block_statement(&tree, view.body, 3) + index :: ast.block_statement(&tree, view.body, 5) + declaration :: ast.statement_decl(&tree, dead) + array :: ast.array_type(&tree, declaration.type?) + try testing.expect_equal(local_size, binding(&resolution, array.count?).data.declaration) + inner :: ast.array_type(&tree, array.element) + count_expression :: ast.binary_expr(&tree, inner.count?) + try testing.expect_equal(size, binding(&resolution, count_expression.left).data.declaration) + try testing.expect_equal(BuiltinType.f64, binding(&resolution, inner.element).data.builtin) + literal_row :: ast.array_element(&tree, declaration.initializer, 0) + try testing.expect_equal(later, binding( + &resolution, ast.array_element(&tree, literal_row, 0), + ).data.declaration) + assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, view.body, 4)) + target :: ast.index_expr(&tree, ast.index_expr(&tree, assignment.target).base) + try testing.expect_equal(dead, binding(&resolution, target.base).data.declaration) + try testing.expect_equal(index, binding(&resolution, target.index).data.declaration) + try testing.expect_equal(later, binding(&resolution, assignment.value).data.declaration) + try testing.expect_equal(BuiltinType.usize, binding( + &resolution, ast.statement_decl(&tree, size).type?, + ).data.builtin) +} + +array_signature_counts_use_package_scope_and_local_counts_see_parameters 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 :: + `size :: 2 + `inspect proc(n usize, a, b [n]i64) [body_size]i64 { + ` body_size :: size + ` local [n]i64 :: [] + ` return [] + `} + 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) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store) + defer deinit(&resolution) + try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) + function :: ast.package_decl(&tree, 1) + view :: ast.function_decl(&tree, function) + parameter :: ast.function_param(&tree, function, 0) + grouped_type :: ast.param(&tree, ast.function_param(&tree, function, 1)).type + try testing.expect_equal(grouped_type, ast.param( + &tree, ast.function_param(&tree, function, 2), + ).type) + signature_count :: ast.array_type(&tree, grouped_type).count? + try testing.expect_equal(BindingKind.invalid, binding(&resolution, signature_count).kind) + result_count :: ast.array_type(&tree, view.return_type).count? + try testing.expect_equal(BindingKind.invalid, binding(&resolution, result_count).kind) + local :: ast.statement_decl(&tree, ast.block_statement(&tree, view.body, 1)) + local_count :: ast.array_type(&tree, local.type?).count? + try testing.expect_equal(BindingKind.declaration, binding(&resolution, local_count).kind) + try testing.expect_equal(parameter, binding(&resolution, local_count).data.declaration) + try testing.expect_equal(BuiltinType.i64, binding( + &resolution, ast.array_type(&tree, local.type?).element, + ).data.builtin) +} + +resolves_every_array_child_error_and_preserves_lexer_diagnostics 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 :: + `main proc() void { + ` return + ` data [missing_count]MissingElement :: [absent, missing_element] + ` (missing_base[missing_index])[missing_outer] = missing_rhs + ` malformed [$]i64 :: [$] + `} + 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) + try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) + resolution := try resolve(mem.c_allocator, &tree, &diagnostic_store) + defer deinit(&resolution) + try testing.expect_equal(10, diagnostics.count(&diagnostic_store)) + body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body + declaration :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 1)) + type :: ast.array_type(&tree, declaration.type?) + assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 2)) + outer :: ast.index_expr(&tree, assignment.target) + inner :: ast.index_expr(&tree, outer.base) + unknowns [8]ast.NodeId :: [ + type.count?, type.element, + ast.array_element(&tree, declaration.initializer, 0), + ast.array_element(&tree, declaration.initializer, 1), + inner.base, inner.index, outer.index, assignment.value, + ] + for (unknowns) |id| { + result :: binding(&resolution, id) + try testing.expect_equal(BindingKind.invalid, result.kind) + try testing.expect_equal(diagnostics.Code.unknown_name, diagnostics.get( + &diagnostic_store, result.data.diagnostic, + )?.code) + } + malformed :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 3)) + malformed_count :: ast.array_type(&tree, malformed.type?).count? + malformed_element :: ast.array_element(&tree, malformed.initializer, 0) + try testing.expect_equal(diagnostics.DiagnosticId(0), binding(&resolution, malformed_count).data.diagnostic) + try testing.expect_equal(diagnostics.DiagnosticId(1), binding(&resolution, malformed_element).data.diagnostic) +}