feat: execute fixed arrays and indexed access

This commit is contained in:
2026-10-10 17:21:43 +02:00
parent cf39481bfc
commit c6a56b7e79
23 changed files with 3474 additions and 256 deletions
+12 -4
View File
@@ -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. `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. `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`. 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`. `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. `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. 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. `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. 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.
+3
View File
@@ -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?
+12
View File
@@ -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
}
+91 -4
View File
@@ -1,15 +1,15 @@
#! grammar: #! grammar:
#! * type ..... -> identifier #! * type ..... -> identifier | '[' (expr | '_') ']' ['mut'] type
#! * binding ... -> :: | := #! * binding ... -> :: | :=
#! * literal .. -> int | float | string | 'true' | 'false' #! * literal .. -> int | float | string | 'true' | 'false'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or' #! * 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 #! * decl ..... -> identifier [type] binding expr
#! * block .... -> '{' {stmt} '}' #! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks) #! * 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. #! 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. #! binary operators associate left; unary operators associate right.
import "@std/debug" import "@std/debug"
@@ -40,6 +40,9 @@ ExprKind :: enum {
binary binary
call call
range range
array_type
array_literal
index
invalid invalid
} }
@@ -86,6 +89,9 @@ NodeKind :: enum {
expr_binary, expr_binary,
expr_call, expr_call,
expr_range, expr_range,
expr_array_type,
expr_array_literal,
expr_index,
stmt_expr, stmt_expr,
stmt_decl, stmt_decl,
stmt_return, stmt_return,
@@ -181,6 +187,19 @@ CallExprView :: struct {
callee NodeId callee NodeId
} }
ArrayTypeView :: struct {
span Span
count ?NodeId
element NodeId
mutable bool
}
IndexView :: struct {
span Span
base NodeId
index NodeId
}
ReturnView :: struct { ReturnView :: struct {
span Span span Span
value ?NodeId value ?NodeId
@@ -336,6 +355,37 @@ add_call proc(
}, span) }, 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 { add_expression_stmt proc(builder @mut Builder, expression NodeId) NodeId ! mem.AllocError {
return try add_node(builder, Node{ return try add_node(builder, Node{
kind = .stmt_expr, kind = .stmt_expr,
@@ -710,6 +760,9 @@ expression_kind proc(kind NodeKind) ExprKind {
.expr_binary: .binary .expr_binary: .binary
.expr_call: .call .expr_call: .call
.expr_range: .range .expr_range: .range
.expr_array_type: .array_type
.expr_array_literal: .array_literal
.expr_index: .index
.invalid_expr: .invalid .invalid_expr: .invalid
else: unreachable 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]) 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 { node_span proc(tree @Tree, id NodeId) Span {
return get_span(&tree.storage, id) return get_span(&tree.storage, id)
} }
+32
View File
@@ -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(ExprKind.literal_int, expr_kind(&tree, statement.initializer))
try testing.expect_equal("value", source_text(&tree, statement.name)) 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)
}
+6
View File
@@ -132,6 +132,12 @@ render_statement proc(
try append_text(renderer, indent) try append_text(renderer, indent)
try append_text(renderer, "declaration ") try append_text(renderer, "declaration ")
try append_text(renderer, ast.source_text(tree, statement.name)) 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") try append_text(renderer, "\n")
} }
+33
View File
@@ -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,
)
}
+17 -1
View File
@@ -37,6 +37,14 @@ Code :: enum {
range_value_not_supported, range_value_not_supported,
range_pointer_capture, range_pointer_capture,
range_index_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, missing_entry,
invalid_entry, invalid_entry,
literal_out_of_range, literal_out_of_range,
@@ -55,7 +63,7 @@ Diagnostic :: struct {
#! owns diagnostic storage and retains its allocator; release with deinit. #! owns diagnostic storage and retains its allocator; release with deinit.
Store :: struct { list arraylist.ArrayList(Diagnostic) } 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. #! retains `allocator` for added diagnostics; the caller owns the returned Store.
init proc(allocator mem.Allocator) 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_value_not_supported = "first-class range values are not implemented",
range_pointer_capture = "range loops do not support pointer captures", range_pointer_capture = "range loops do not support pointer captures",
range_index_capture = "range loops do not support index 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", missing_entry = "program requires a main function",
invalid_entry = "main must be a function with no parameters", invalid_entry = "main must be a function with no parameters",
literal_out_of_range = "numeric literal is outside its runtime type's range", literal_out_of_range = "numeric literal is outside its runtime type's range",
+236 -17
View File
@@ -6,6 +6,17 @@ import "@source/diagnostics"
Outcome :: struct { Outcome :: struct {
value ir.Value = ir.Value{} value ir.Value = ir.Value{}
trap ?diagnostics.DiagnosticId = null 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 @hide:file
@@ -23,6 +34,15 @@ GlobalState :: struct {
value ir.Value = ir.Value{} 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 @hide:file
Frame :: struct { Frame :: struct {
function ir.FunctionId function ir.FunctionId
@@ -32,10 +52,12 @@ Frame :: struct {
global ?usize = null global ?usize = null
locals []mut Slot locals []mut Slot
values []mut ir.Value 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. #! executes typed IR with invocation/global storage released on every exit.
#! returned string IDs resolve through `module`; runtime source errors are traps, not infrastructure errors. #! array results own flat storage released by `deinit`; string IDs and shapes resolve through `module`.
run proc( run proc(
allocator mem.Allocator, allocator mem.Allocator,
module @ir.Module, module @ir.Module,
@@ -51,6 +73,22 @@ run proc(
globals :: try mem.alloc(GlobalState, allocator, module.globals.items.len) globals :: try mem.alloc(GlobalState, allocator, module.globals.items.len)
defer mem.free(allocator, globals) defer mem.free(allocator, globals)
mem.set(globals, GlobalState{}) 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) frames := arraylist.init(Frame, allocator)
defer { defer {
for (frames.items) |frame| free_frame(allocator, frame) for (frames.items) |frame| free_frame(allocator, frame)
@@ -81,16 +119,25 @@ run proc(
value = frames.items[frame_index].values[usize(terminator.value)] value = frames.items[frame_index].values[usize(terminator.value)]
} }
completed :: frames.items[frame_index] completed :: frames.items[frame_index]
_ = arraylist.pop(&frames) # transfer leaves while the completed frame still owns its backing.
free_frame(allocator, completed)
if completed.global |global_index| { if completed.global |global_index| {
globals[global_index].value = value copy_value(&globals[global_index].value, value)
globals[global_index].status = .ready globals[global_index].status = .ready
} }
if (frames.items.len == 0) return Outcome{ value = value } if (frames.items.len == 1) {
if completed.destination != ir.no_value { if (value.type == .array) {
frames.items[frames.items.len - 1].values[usize(completed.destination)] = value 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 continue
} }
@@ -107,10 +154,9 @@ run proc(
value = local.value value = local.value
} }
.store_local: { .store_local: {
frames.items[frame_index].locals[usize(instruction.slot)] = Slot{ local :: &frames.items[frame_index].locals[usize(instruction.slot)]
initialized = true, copy_value(&local.value, frames.items[frame_index].values[usize(instruction.left)])
value = frames.items[frame_index].values[usize(instruction.left)], local.initialized = true
}
continue continue
} }
.assign_local: { .assign_local: {
@@ -118,7 +164,7 @@ run proc(
if (!local.initialized) { if (!local.initialized) {
return try runtime_trap(diagnostic_store, .write_before_initialization, instruction.span) 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 continue
} }
.reset_local: { .reset_local: {
@@ -152,13 +198,107 @@ run proc(
# frame indices survive stack growth; pointers into frames.items do not. # frame indices survive stack growth; pointers into frames.items do not.
for (0..instruction.arguments_count) |argument_index| { for (0..instruction.arguments_count) |argument_index| {
argument :: block.arguments.items[instruction.arguments_start + argument_index] argument :: block.arguments.items[instruction.arguments_start + argument_index]
frames.items[frame_index + 1].locals[argument_index] = Slot{ local :: &frames.items[frame_index + 1].locals[argument_index]
initialized = true, copy_value(&local.value, frames.items[frame_index].values[usize(argument)])
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 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: { .negate: {
operand :: frames.items[frame_index].values[usize(instruction.left)] operand :: frames.items[frame_index].values[usize(instruction.left)]
if instruction.type == .float { if instruction.type == .float {
@@ -187,6 +327,13 @@ run proc(
else: unreachable else: unreachable
} }
value = ir.Value{ type = .float, data = ir.ValueData{ real = result } } 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 { } else {
result :: checked_integer(instruction.op, left.data.integer, right.data.integer) result :: checked_integer(instruction.op, left.data.integer, right.data.integer)
if result |result_integer| { if result |result_integer| {
@@ -210,6 +357,15 @@ run proc(
.greater_equal: left.data.integer >= right.data.integer .greater_equal: left.data.integer >= right.data.integer
else: unreachable 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 { .float: result = match instruction.op {
.equal: left.data.real == right.data.real .equal: left.data.real == right.data.real
.not_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 { 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 unreachable
@@ -251,6 +407,17 @@ push_frame proc(
mem.set(locals, Slot{}) mem.set(locals, Slot{})
values :: try mem.alloc(ir.Value, allocator, signature.value_count) values :: try mem.alloc(ir.Value, allocator, signature.value_count)
errdefer mem.free(allocator, values) 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. # typed IR assigns each temporary before use; only local slots need initialization flags.
try arraylist.append(frames, Frame{ try arraylist.append(frames, Frame{
function = function, function = function,
@@ -259,6 +426,8 @@ push_frame proc(
global = global, global = global,
locals = locals, locals = locals,
values = values, values = values,
aggregate = aggregate,
addresses = addresses,
}) })
} }
@@ -266,6 +435,8 @@ push_frame proc(
free_frame proc(allocator mem.Allocator, frame Frame) void { free_frame proc(allocator mem.Allocator, frame Frame) void {
mem.free(allocator, frame.locals) mem.free(allocator, frame.locals)
mem.free(allocator, frame.values) mem.free(allocator, frame.values)
mem.free(allocator, frame.aggregate)
mem.free(allocator, frame.addresses)
} }
@hide:file @hide:file
@@ -305,3 +476,51 @@ checked_integer proc(op ir.Op, left i64, right i64) ?i64 {
else: unreachable 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
}
}
+684 -31
View File
@@ -16,6 +16,7 @@ add_function proc(
errdefer { errdefer {
arraylist.deinit(&function.parameters) arraylist.deinit(&function.parameters)
arraylist.deinit(&function.locals) arraylist.deinit(&function.locals)
arraylist.deinit(&function.array_storage)
for (function.blocks.items) |block| { for (function.blocks.items) |block| {
owned := block owned := block
arraylist.deinit(&owned.instructions) arraylist.deinit(&owned.instructions)
@@ -37,6 +38,7 @@ add_function proc(
@hide:file @hide:file
emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! mem.AllocError { 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) 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, function_value(6, noop))
try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(7), left = ir.ValueId(6) }) 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)) 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(outcome.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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 = .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) }) 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)) 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(initial.trap == null)
try testing.expect_equal(i64(42), initial.value.data.integer) try testing.expect_equal(i64(42), initial.value.data.integer)
module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = integer(0, 30) 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(repeated.trap == null)
try testing.expect_equal(i64(62), repeated.value.data.integer) 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 = .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) }) 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)) 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(outcome.trap == null)
try testing.expect_equal(ir.Type.string, outcome.value.type) try testing.expect_equal(ir.Type.string, outcome.value.type)
try testing.expect_equal([]u8, text, ir.string_bytes(&module, outcome.value.data.string)) 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 module.entry = main
try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0, span = span }) 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)) 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) try expect_trap(premature, &store, .read_before_initialization, span)
first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1) 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 = 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 } }) 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 } 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) 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) 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) defer diagnostics.deinit(&store)
entry_id :: try diagnostics.add(&store, .missing_entry, diagnostics.Span{ start = 0, end = 0 }) entry_id :: try diagnostics.add(&store, .missing_entry, diagnostics.Span{ start = 0, end = 0 })
module.entry_diagnostic = entry_id 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?) try testing.expect_equal(entry_id, absent.trap?)
invalid_id :: try diagnostics.add(&store, .unknown_name, diagnostics.Span{ start = 12, end = 17 }) 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) 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 module.entry = main
try emit(&module, main, integer(0, 42)) try emit(&module, main, integer(0, 42))
return_value(&module, main, ir.ValueId(0)) 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(success.trap == null)
try testing.expect_equal(i64(42), success.value.data.integer) 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) 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) }) 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(invalid_id, trapped.trap?)
try testing.expect_equal(usize(2), diagnostics.count(&store)) 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, 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 }) 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)) 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| { if case.expected |expected| {
try testing.expect(outcome.trap == null) try testing.expect(outcome.trap == null)
try testing.expect_equal(expected, outcome.value.data.integer) 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, integer(0, minval!(i64)))
try emit(&module, main, ir.Instruction{ op = .negate, type = .int, destination = ir.ValueId(1), left = ir.ValueId(0), span = span }) 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)) 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) try expect_trap(trapped, &store, .integer_overflow, span)
module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = integer(0, maxval!(i64)) 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(success.trap == null)
try testing.expect_equal(-maxval!(i64), success.value.data.integer) 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 = .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) }) 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)) 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(result.trap == null)
try testing.expect_equal(ir.Type.float, result.value.type) try testing.expect_equal(ir.Type.float, result.value.type)
try testing.expect_equal(f64(-10.5), result.value.data.real) 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[0].constant.data.real = 0
module.functions.items[usize(main)].blocks.items[0].instructions.items[1].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.trap == null)
try testing.expect(nan.value.data.real != nan.value.data.real) 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[0].constant.data.real = 1
module.functions.items[usize(main)].blocks.items[0].instructions.items[2].op = .add 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.trap == null)
try testing.expect(infinite.value.data.real < f64(-1000000000)) try testing.expect(infinite.value.data.real < f64(-1000000000))
try testing.expect_equal(infinite.value.data.real, infinite.value.data.real * f64(2)) 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) }) 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 } state := AllocationState{ remaining = 2048 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
_ = run(allocator, &module, &store) catch |error| { unexpected := run(allocator, &module, &store) catch |error| {
match error { match error {
.out_of_memory: {} .out_of_memory: {}
else: return .expectation_failed 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)) try testing.expect_equal(usize(0), diagnostics.count(&store))
return return
} }
deinit(&unexpected)
return .expectation_failed 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| { for (usize(0)..usize(16)) |limit| {
state := AllocationState{ remaining = limit } state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
outcome :: run(allocator, &module, &store) catch |error| { outcome := run(allocator, &module, &store) catch |error| {
match error { match error {
.out_of_memory: {} .out_of_memory: {}
else: return .expectation_failed 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) try testing.expect_equal(usize(0), state.live)
continue continue
} }
defer deinit(&outcome)
succeeded = true succeeded = true
try testing.expect(outcome.trap == null) try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(i64(42), outcome.value.data.integer)
deinit(&outcome)
try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), state.live)
} }
try testing.expect(succeeded) 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 } module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id }
state := AllocationState{ remaining = 64 } state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } 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?) try testing.expect_equal(trap_id, trapped.trap?)
deinit(&trapped)
try testing.expect_equal(usize(0), state.live) 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) defer diagnostics.deinit(&store)
state := AllocationState{ remaining = 64 } state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
_ = run(allocator, &module, &store) catch |error| { unexpected := run(allocator, &module, &store) catch |error| {
match error { match error {
.out_of_memory: {} .out_of_memory: {}
else: return .expectation_failed 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)) try testing.expect_equal(usize(0), diagnostics.count(&store))
return return
} }
deinit(&unexpected)
return .expectation_failed return .expectation_failed
} }
@@ -500,6 +524,7 @@ add_block proc(module @mut ir.Module, function ir.FunctionId) ir.BlockId ! mem.A
@hide:file @hide:file
emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! mem.AllocError { 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) 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) } module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ value = ir.ValueId(4) }
state := AllocationState{ remaining = 2048 } state := AllocationState{ remaining = 2048 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } 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(outcome.trap == null)
try testing.expect_equal(i64(130), outcome.value.data.integer) 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(0), state.live)
try testing.expect_equal(usize(1), diagnostics.count(&store)) try testing.expect_equal(usize(1), diagnostics.count(&store))
# trapping after returning through many heap frames also releases every live frame. # 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 } 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?) try testing.expect_equal(trap_id, trapped.trap?)
deinit(&trapped)
try testing.expect_equal(usize(0), state.live) 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 .greater_equal: left >= right
else: unreachable 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(outcome.trap == null)
try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(ir.Type.bool, outcome.value.type)
try testing.expect_equal(expected, outcome.value.data.boolean) 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) 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 }) 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)) 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(outcome.trap == null)
try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(ir.Type.bool, outcome.value.type)
try testing.expect_equal(op == .not_equal, outcome.value.data.boolean) 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 equal :: left == right
expected_true :: if (op == .equal) equal else !equal expected_true :: if (op == .equal) equal else !equal
expected i64 :: if (expected_true) 42 else 7 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(outcome.trap == null)
try testing.expect_equal(expected, outcome.value.data.integer) 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 = .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 }) try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(2), slot = 0 })
return_value(&module, main, ir.ValueId(2)) 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(outcome.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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{ try emit(&module, main, ir.Instruction{
op = op, type = .int, destination = destination, slot = 0, left = ir.ValueId(0), span = span, 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 expect_trap(outcome, &store, codes[index], span)
try testing.expect_equal(usize(1), diagnostics.count(&store)) 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| { for (usize(0)..usize(16)) |limit| {
state := AllocationState{ remaining = limit } state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
outcome :: run(allocator, &module, &store) catch |error| { outcome := run(allocator, &module, &store) catch |error| {
match error { match error {
.out_of_memory: {} .out_of_memory: {}
else: return .expectation_failed 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) try testing.expect_equal(usize(0), state.live)
continue continue
} }
defer deinit(&outcome)
succeeded = true succeeded = true
try expect_trap(outcome, &store, .write_before_initialization, span) try expect_trap(outcome, &store, .write_before_initialization, span)
deinit(&outcome)
try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), state.live)
} }
try testing.expect(succeeded) try testing.expect(succeeded)
@@ -767,7 +803,7 @@ assignment_diagnostic_allocation_failure_releases_runtime_storage test {
defer diagnostics.deinit(&store) defer diagnostics.deinit(&store)
state := AllocationState{ remaining = 64 } state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
_ = run(allocator, &module, &store) catch |error| { unexpected := run(allocator, &module, &store) catch |error| {
match error { match error {
.out_of_memory: {} .out_of_memory: {}
else: return .expectation_failed 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)) try testing.expect_equal(usize(0), diagnostics.count(&store))
return return
} }
deinit(&unexpected)
return .expectation_failed 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(1), left = ir.ValueId(0) })
try emit(&module, main, ir.Instruction{ op = .logical_not, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(1) }) 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)) 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(inverted.trap == null)
try testing.expect_equal(ir.Type.bool, inverted.value.type) try testing.expect_equal(ir.Type.bool, inverted.value.type)
try testing.expect_equal(expected[index], inverted.value.data.boolean) try testing.expect_equal(expected[index], inverted.value.data.boolean)
return_value(&module, main, ir.ValueId(2)) 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(composed.trap == null)
try testing.expect_equal(operand, composed.value.data.boolean) try testing.expect_equal(operand, composed.value.data.boolean)
try testing.expect_equal(usize(0), diagnostics.count(&store)) 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)
}
+45
View File
@@ -8,6 +8,8 @@ LocalId :: distinct u32
ValueId :: distinct u32 ValueId :: distinct u32
BlockId :: distinct u32 BlockId :: distinct u32
StringId :: distinct u32 StringId :: distinct u32
ArrayId :: distinct u32
no_array :: maxval!(ArrayId)
no_value :: maxval!(ValueId) no_value :: maxval!(ValueId)
Type :: enum { Type :: enum {
@@ -18,20 +20,44 @@ Type :: enum {
string, string,
void, void,
function, function,
array,
usize,
address,
} }
ValueData :: union { ValueData :: union {
integer i64 integer i64
unsigned u64
real f64 real f64
boolean bool boolean bool
string StringId string StringId
function FunctionId function FunctionId
elements []mut Value
address u32
} }
Value :: struct { Value :: struct {
type Type = .void type Type = .void
array_type ArrayId = no_array
data ValueData = ValueData{ integer = 0 } 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 { Op :: enum {
constant constant
load_local load_local
@@ -39,6 +65,13 @@ Op :: enum {
assign_local assign_local
reset_local reset_local
load_global load_global
array_literal
index
address_local
address_global
index_address
load_address
store_address
negate negate
logical_not logical_not
add add
@@ -57,6 +90,8 @@ Op :: enum {
Instruction :: struct { Instruction :: struct {
op Op op Op
type Type = .void type Type = .void
array_type ArrayId = no_array
pointee Type = .invalid
span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 } span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 }
destination ValueId = no_value destination ValueId = no_value
left ValueId = no_value left ValueId = no_value
@@ -93,6 +128,10 @@ Function :: struct {
parameters arraylist.ArrayList(Type) parameters arraylist.ArrayList(Type)
locals arraylist.ArrayList(Type) locals arraylist.ArrayList(Type)
result Type = .void 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) blocks arraylist.ArrayList(Block)
entry BlockId = BlockId(0) entry BlockId = BlockId(0)
value_count usize = 0 value_count usize = 0
@@ -101,6 +140,7 @@ Function :: struct {
Global :: struct { Global :: struct {
type Type type Type
array_type ArrayId = no_array
initializer FunctionId initializer FunctionId
span diagnostics.Span span diagnostics.Span
} }
@@ -118,6 +158,7 @@ Module :: struct {
functions arraylist.ArrayList(Function) functions arraylist.ArrayList(Function)
globals arraylist.ArrayList(Global) globals arraylist.ArrayList(Global)
strings arraylist.ArrayList(StringStorage) strings arraylist.ArrayList(StringStorage)
arrays arraylist.ArrayList(ArrayType)
entry ?FunctionId = null entry ?FunctionId = null
entry_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic entry_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
} }
@@ -128,6 +169,7 @@ init proc(allocator mem.Allocator) Module {
functions = arraylist.init(allocator), functions = arraylist.init(allocator),
globals = arraylist.init(allocator), globals = arraylist.init(allocator),
strings = 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), parameters = arraylist.init(allocator),
locals = arraylist.init(allocator), locals = arraylist.init(allocator),
blocks = 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.parameters)
arraylist.deinit(&function.locals) arraylist.deinit(&function.locals)
arraylist.deinit(&function.blocks) arraylist.deinit(&function.blocks)
arraylist.deinit(&function.array_storage)
} }
for (module.strings.items) |string| for (module.strings.items) |string|
mem.free(module.allocator, string.allocation) mem.free(module.allocator, string.allocation)
@@ -171,4 +215,5 @@ deinit proc(module @mut Module) void {
arraylist.deinit(&module.functions) arraylist.deinit(&module.functions)
arraylist.deinit(&module.globals) arraylist.deinit(&module.globals)
arraylist.deinit(&module.strings) arraylist.deinit(&module.strings)
arraylist.deinit(&module.arrays)
} }
+1
View File
@@ -32,6 +32,7 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
{ "false", .false }, { "false", .false },
{ "and", .and }, { "and", .and },
{ "or", .or }, { "or", .or },
{ "mut", .mut },
]) ])
#! owns token storage and retains its allocator; release with deinit. #! owns token storage and retains its allocator; release with deinit.
+19
View File
@@ -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(u32(10), state.tokens.items[4].span.end)
try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) 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))
}
+1
View File
@@ -24,6 +24,7 @@ TokenKind :: enum {
break, continue break, continue
true, false true, false
and, or and, or
mut
# literals # literals
ident, string ident, string
+203 -13
View File
@@ -30,6 +30,7 @@ State :: struct {
diagnostics @mut diagnostics.Store diagnostics @mut diagnostics.Store
module @mut ir.Module module @mut ir.Module
storage []mut Storage storage []mut Storage
arrays []mut ir.ArrayId
arguments arraylist.ArrayList(ir.ValueId) arguments arraylist.ArrayList(ir.ValueId)
function usize = 0 function usize = 0
block ir.BlockId = ir.BlockId(0) block ir.BlockId = ir.BlockId(0)
@@ -51,6 +52,9 @@ lower proc(
storage :: try mem.alloc(Storage, allocator, ast.node_count(tree)) storage :: try mem.alloc(Storage, allocator, ast.node_count(tree))
defer mem.free(allocator, storage) defer mem.free(allocator, storage)
mem.set(storage, 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{ state := State{
tree = tree, tree = tree,
resolution = resolution, resolution = resolution,
@@ -58,6 +62,7 @@ lower proc(
diagnostics = diagnostic_store, diagnostics = diagnostic_store,
module = &module, module = &module,
storage = storage, storage = storage,
arrays = arrays,
arguments = arraylist.init(ir.ValueId, allocator), arguments = arraylist.init(ir.ValueId, allocator),
} }
defer arraylist.deinit(&state.arguments) defer arraylist.deinit(&state.arguments)
@@ -75,6 +80,7 @@ lower proc(
global :: u32(module.globals.items.len) global :: u32(module.globals.items.len)
try arraylist.append(&module.globals, ir.Global{ try arraylist.append(&module.globals, ir.Global{
type = lower_type(typecheck.type_of(checked, id)), type = lower_type(typecheck.type_of(checked, id)),
array_type = try array_shape(&state, typecheck.type_of(checked, id)),
initializer = initializer, initializer = initializer,
span = ast.node_span(tree, id), span = ast.node_span(tree, id),
}) })
@@ -99,6 +105,8 @@ lower proc(
lower_type proc(type typecheck.Type) ir.Type { lower_type proc(type typecheck.Type) ir.Type {
return match type.kind { return match type.kind {
.int: .int .int: .int
.usize: .usize
.array: .array
.float: .float .float: .float
.bool: .bool .bool: .bool
.string: .string .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 @hide:file
add_function proc(module @mut ir.Module, span diagnostics.Span) ir.FunctionId ! mem.AllocError { add_function proc(module @mut ir.Module, span diagnostics.Span) ir.FunctionId ! mem.AllocError {
id :: ir.FunctionId(module.functions.items.len) 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 @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] function := &state.module.functions.items[state.function]
operation := instruction 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) operation.destination = ir.ValueId(function.value_count)
function.value_count += 1 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) try arraylist.append(&function.blocks.items[usize(state.block)].instructions, operation)
return operation.destination return operation.destination
} }
@hide:file @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 }) 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.block = ir.BlockId(0)
state.terminated = false state.terminated = false
state.module.functions.items[state.function].result = global.type 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 if (trap_binding(state, id)) return
declaration :: ast.statement_decl(state.tree, id) declaration :: ast.statement_decl(state.tree, id)
if (declaration.type) |annotation| if (trap_type(state, annotation)) return 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 state.terminated = false
function := &state.module.functions.items[state.function] function := &state.module.functions.items[state.function]
function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type)) 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| { for (0..ast.function_param_count(tree, id)) |i| {
parameter :: ast.function_param(tree, id, i) parameter :: ast.function_param(tree, id, i)
type :: lower_type(typecheck.type_of(state.checked, parameter)) type :: lower_type(typecheck.type_of(state.checked, parameter))
state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(function.locals.items.len) } state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
try arraylist.append(&function.parameters, type) try arraylist.append(&function.parameters, type)
try arraylist.append(&function.locals, 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) try predeclare_locals(state, declaration.body)
# signature errors are entry traps; body errors are reached in statement order. # 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 @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| { for (0..ast.block_statement_count(state.tree, block)) |i| {
statement :: ast.block_statement(state.tree, block, i) statement :: ast.block_statement(state.tree, block, i)
match ast.stmt_kind(state.tree, statement) { 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] function := &state.module.functions.items[state.function]
state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) } 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 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: { .if_stmt: {
conditional :: ast.if_stmt(state.tree, statement) 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) loop :: ast.for_stmt(state.tree, statement)
function := &state.module.functions.items[state.function] function := &state.module.functions.items[state.function]
slot :: u32(function.locals.items.len) 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 } state.storage[usize(statement)] = Storage{ kind = .local, index = slot }
if (loop.capture) |capture| state.storage[usize(capture)] = state.storage[usize(statement)] if (loop.capture) |capture| state.storage[usize(capture)] = state.storage[usize(statement)]
if (loop.index) |index| { 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) end :: try lower_expression(state, interval.end)
if (state.terminated or trap_type(state, loop.iterable) or trap_type(state, id)) return if (state.terminated or trap_type(state, loop.iterable) or trap_type(state, id)) return
slot :: state.storage[usize(id)].index 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 origin :: state.block
condition :: try add_block(state) condition :: try add_block(state)
body :: 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, kind = .jump, target = condition, span = loop.span,
} }
state.block = condition 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 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 }) 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{ 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.block = update
state.terminated = false 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 }) 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. # 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{ 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, kind = .branch, value = finished, target = exit, alternative = increment, span = interval.span,
} }
state.block = increment state.block = increment
one :: try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } }, interval.span) unit := ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } }
next :: try emit(state, ir.Instruction{ op = .add, type = .int, left = last, right = one, span = interval.span }) if (family == .usize) unit = ir.Value{ type = .usize, data = ir.ValueData{ unsigned = 1 } }
_ = try emit(state, ir.Instruction{ op = .store_local, type = .int, left = next, slot = slot, span = interval.span }) 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{ state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = condition, span = loop.span, 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_stmt: {
assignment :: ast.assignment_stmt(tree, id) 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 if (trap_binding(state, assignment.target) or trap_type(state, id)) return
value :: try lower_expression(state, assignment.value) value :: try lower_expression(state, assignment.value)
if (state.terminated) return 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{ _ = try emit(state, ir.Instruction{
op = .assign_local, op = .assign_local,
type = lower_type(typecheck.type_of(state.checked, assignment.target)), 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, left = value,
slot = state.storage[usize(binding.data.declaration)].index, slot = state.storage[usize(binding.data.declaration)].index,
span = ast.node_span(tree, assignment.target), 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{ _ = try emit(state, ir.Instruction{
op = .store_local, op = .store_local,
type = lower_type(typecheck.type_of(state.checked, id)), type = lower_type(typecheck.type_of(state.checked, id)),
array_type = try array_shape(state, typecheck.type_of(state.checked, id)),
left = value, left = value,
slot = state.storage[usize(id)].index, slot = state.storage[usize(id)].index,
span = declaration.span, 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 }) 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 @hide:file
lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree tree :: state.tree
span :: ast.node_span(tree, id) span :: ast.node_span(tree, id)
type :: lower_type(typecheck.type_of(state.checked, 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) { match ast.expr_kind(tree, id) {
.invalid: { .invalid: {
invalid :: ast.invalid(tree, id) 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. # ranges have no physical value; only lower_for decomposes an iterable.
if (!trap_type(state, id)) unreachable 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: { .literal_int, .literal_float: {
if (trap_type(state, id)) return ir.no_value if (trap_type(state, id)) return ir.no_value
return try lower_number(state, id) 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, type = .function,
data = ir.ValueData{ function = ir.FunctionId(location.index) }, data = ir.ValueData{ function = ir.FunctionId(location.index) },
}, span) }, span)
.global: return try emit(state, ir.Instruction{ op = .load_global, 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, 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 .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{ return try emit(state, ir.Instruction{
op = .call, op = .call,
type = type, type = type,
array_type = shape,
left = callee, left = callee,
arguments_start = start, arguments_start = start,
arguments_count = count, 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| { if (integer_magnitude(text, u64(maxval!(i64)))) |magnitude| {
return try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = i64(magnitude) } }, span) 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 { } else {
buffer :: try mem.alloc(u8, state.module.allocator, text.len + 1) buffer :: try mem.alloc(u8, state.module.allocator, text.len + 1)
defer mem.free(state.module.allocator, buffer) defer mem.free(state.module.allocator, buffer)
+409 -57
View File
@@ -68,7 +68,8 @@ executes_calls_locals_arithmetic_and_nested_argument_slices test {
module := try compile_owned(source, &diagnostic_store) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(ir.Type.string, outcome.value.type) 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")) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(ir.Type.float, outcome.value.type) try testing.expect_equal(ir.Type.float, outcome.value.type)
try testing.expect_equal(f64(0.5), outcome.value.data.real) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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(again.trap == null)
try testing.expect_equal(i64(42), again.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(ir.Type.void, outcome.value.type) 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))? diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(diagnostics.Code.unknown_name, diagnostic.code) 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")) 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(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect(diagnostics.count(&diagnostic_store) > 0) 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(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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 code diagnostics.Code :: if (i == 0) .read_before_initialization else .initialization_cycle
try expect_trap(outcome, &diagnostic_store, code) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(expected[i], outcome.value.data.integer) 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) defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) 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 code := diagnostics.Code.integer_overflow
if (i < 2) code = .literal_out_of_range if (i < 2) code = .literal_out_of_range
if (i >= 6) code = .invalid_escape if (i >= 6) code = .invalid_escape
@@ -285,7 +296,8 @@ validates_main_and_preserves_first_declaration test {
defer diagnostics.deinit(&diagnostic_store) defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) 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) { if (i == 3) {
try testing.expect(outcome.trap == null) try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect(diagnostics.count(&diagnostic_store) > 0) 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) { if (i == 0) {
try testing.expect(outcome.trap == null) try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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, &diagnostic_store,
) )
defer ir.deinit(&module) 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(outcome.trap == null)
try testing.expect_equal(ir.Type.float, outcome.value.type) try testing.expect_equal(ir.Type.float, outcome.value.type)
try testing.expect_equal(f64(18446744073709551616.0), outcome.value.data.real) 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) for (".0\n}\n") |byte| try arraylist.append(&source, byte)
oversized := try compile_owned(source.items, &diagnostic_store) oversized := try compile_owned(source.items, &diagnostic_store)
defer ir.deinit(&oversized) 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) 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) defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source.items, &diagnostic_store) module := try compile_owned(source.items, &diagnostic_store)
defer ir.deinit(&module) 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) { if (count == 307) {
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
try testing.expect(outcome.trap == null) try testing.expect(outcome.trap == null)
@@ -436,8 +452,11 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
` local :: value ` local :: value
` return local ` return local
`} `}
`rows proc(value [2][2]string) [2][2]string { return value }
`main proc() string { `main proc() string {
` computed float :: factor * 2 ` computed float :: factor * 2
` aggregate :: rows([[text, "second"], ["third", text]])
` (aggregate[1])[1]
` remaining := 2 ` remaining := 2
` for 0..=2 |item| { ` for 0..=2 |item| {
` saved :: identity(text) ` saved :: identity(text)
@@ -491,9 +510,11 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
continue continue
} }
errdefer ir.deinit(&module) 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(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) ir.deinit(&module)
try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), state.live)
succeeded = true succeeded = true
@@ -520,7 +541,8 @@ recursive_factorial_and_conditional_returns_execute_after_frontend_cleanup test
module := try compile_owned(source, &diagnostic_store) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(i64(120), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(ir.Type.bool, outcome.value.type)
try testing.expect(outcome.value.data.boolean) 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) defer ir.deinit(&module)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? 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) { if (index == 0 or index == 2 or index == 4) {
try testing.expect(outcome.trap == null) try testing.expect(outcome.trap == null)
if (index == 4) try testing.expect_equal(ir.Type.void, outcome.value.type) 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) defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) 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) { if (index == 2) {
try testing.expect(outcome.trap == null) try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(i64(120), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(expected_types[index], outcome.value.type) try testing.expect_equal(expected_types[index], outcome.value.type)
match index { match index {
@@ -768,7 +798,8 @@ forward_assignments_check_initialization_after_evaluating_the_rhs test {
module := try compile_owned(source, &diagnostic_store) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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]) try expect_trap(outcome, &diagnostic_store, codes[index])
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "later")) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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]) try expect_trap(outcome, &diagnostic_store, codes[index])
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "later")) 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)) try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(codes[index], original.code) 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(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) 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) defer ir.deinit(&module)
try testing.expect(diagnostics.count(&diagnostic_store) > 0) try testing.expect(diagnostics.count(&diagnostic_store) > 0)
count :: diagnostics.count(&diagnostic_store) 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(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(count, diagnostics.count(&diagnostic_store)) 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))? diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(diagnostics.Code.missing_return, diagnostic.code) try testing.expect_equal(diagnostics.Code.missing_return, diagnostic.code)
} else try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) } 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) { if (index == 0) {
try testing.expect(outcome.trap == null) try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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)) try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
state := AllocationState{ remaining = 64 } state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } 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(outcome.trap == null)
try testing.expect_equal(expected[index], outcome.value.data.integer) try testing.expect_equal(expected[index], outcome.value.data.integer)
interpreter.deinit(&outcome)
try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), state.live)
if (index == 0) allocations = usize(64) - state.remaining if (index == 0) allocations = usize(64) - state.remaining
else try testing.expect_equal(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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(ir.Type.bool, outcome.value.type)
try testing.expect_equal(expected[index], outcome.value.data.boolean) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) defer ir.deinit(&module)
count :: diagnostics.count(&diagnostic_store) count :: diagnostics.count(&diagnostic_store)
try testing.expect(count > 0) 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(outcome.trap == null)
try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(ir.Type.bool, outcome.value.type)
try testing.expect(outcome.value.data.boolean) 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" operand []u8 :: if (index >= 2 and index <= 4) "1" else "absent"
try testing.expect_equal(code, original.code) try testing.expect_equal(code, original.code)
try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], operand)) 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(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(2, diagnostics.count(&diagnostic_store)) 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?)? original :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect_equal(diagnostics.Code.unknown_name, original.code) 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")) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect(outcome.value.data.boolean) try testing.expect(outcome.value.data.boolean)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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]) try expect_trap(outcome, &diagnostic_store, codes[index])
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], spans[index])) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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| { for (usize(0)..usize(64)) |limit| {
state := AllocationState{ remaining = limit } state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } 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) try testing.expect(error == .out_of_memory)
failures += 1 failures += 1
try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), state.live)
continue continue
} }
defer interpreter.deinit(&outcome)
try testing.expect(outcome.trap == null) try testing.expect(outcome.trap == null)
try testing.expect(outcome.value.data.boolean) try testing.expect(outcome.value.data.boolean)
interpreter.deinit(&outcome)
try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), state.live)
succeeded = true succeeded = true
break break
@@ -1181,7 +1228,8 @@ skipped_rhs_does_not_hide_a_demanded_outer_type_mismatch test {
defer ir.deinit(&module) defer ir.deinit(&module)
count :: diagnostics.count(&diagnostic_store) count :: diagnostics.count(&diagnostic_store)
try testing.expect_equal(2, count) 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 expect_trap(outcome, &diagnostic_store, codes[index])
try testing.expect_equal(count, diagnostics.count(&diagnostic_store)) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) module := try compile_owned(source.items, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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))? original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(codes[index], original.code) try testing.expect_equal(codes[index], original.code)
try testing.expect(mem.eql(source[usize(original.span.start)..usize(original.span.end)], keywords[index])) 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(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) 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)) try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(codes[index], original.code) 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(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) 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)) try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(diagnostics.Code.unknown_name, original.code) 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(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) 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 expect_trap(outcome, &diagnostic_store, .unknown_name)
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(ir.Type.int, outcome.value.type)
try testing.expect_equal(expected[index], outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) 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]) try expect_trap(outcome, &diagnostic_store, codes[index])
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "later")) 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) defer ir.deinit(&module)
count :: diagnostics.count(&diagnostic_store) count :: diagnostics.count(&diagnostic_store)
try testing.expect(count > 0) 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(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(i64(42), outcome.value.data.integer)
try testing.expect_equal(count, diagnostics.count(&diagnostic_store)) 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)) try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))? original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
try testing.expect_equal(codes[index], original.code) 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(diagnostics.DiagnosticId(0), outcome.trap?)
try testing.expect_equal(1, diagnostics.count(&diagnostic_store)) 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) module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module) defer ir.deinit(&module)
before :: diagnostics.count(&diagnostic_store) 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 code diagnostics.Code :: if (index == 3) .unknown_name else .integer_overflow
try expect_trap(outcome, &diagnostic_store, code) try expect_trap(outcome, &diagnostic_store, code)
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)? 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)) try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
state := AllocationState{ remaining = 64 } state := AllocationState{ remaining = 64 }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } 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(outcome.trap == null)
try testing.expect_equal(expected[index], outcome.value.data.integer) try testing.expect_equal(expected[index], outcome.value.data.integer)
interpreter.deinit(&outcome)
try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), state.live)
if (index == 0) allocations = usize(64) - state.remaining if (index == 0) allocations = usize(64) - state.remaining
else try testing.expect_equal(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)
}
+40 -6
View File
@@ -13,6 +13,7 @@ import "@source/sema"
import "@source/ir" import "@source/ir"
import "@source/lower" import "@source/lower"
import "@source/interpreter" import "@source/interpreter"
typecheck :: import "@source/sema/check" typecheck :: import "@source/sema/check"
test import "@std/mem" test import "@std/mem"
@@ -65,6 +66,7 @@ infrastructure_message proc(error DriverError) []u8 {
return match error { return match error {
.out_of_memory: "out of memory" .out_of_memory: "out of memory"
.diagnostic_limit_reached: "diagnostic limit reached" .diagnostic_limit_reached: "diagnostic limit reached"
.storage_limit_reached: "aggregate storage limit reached"
.source_too_large: "source file is too large" .source_too_large: "source file is too large"
.out_of_space: "identifier storage is full" .out_of_space: "identifier storage is full"
.open_failed: "cannot open source file" .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) defer diagnostics.deinit(&diagnostic_store)
module := try compile(allocator, source.items, &diagnostic_store) module := try compile(allocator, source.items, &diagnostic_store)
defer ir.deinit(&module) 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) 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) 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 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 @hide:file
print_result proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError { 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 { match value.type {
.int: try io.print("{}\n", output, {value.data.integer}) .int: try io.print("{}", output, {value.data.integer})
.float: try io.print("{d}\n", output, {value.data.real}) .usize: try io.print("{}", output, {value.data.unsigned})
.bool: try io.print("{}\n", output, {value.data.boolean}) .float: try io.print("{d}", output, {value.data.real})
.string: try io.print("{s}\n", output, {ir.string_bytes(module, value.data.string)}) .bool: try io.print("{}", output, {value.data.boolean})
.void: {} .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 else: unreachable
} }
} }
+213 -53
View File
@@ -103,7 +103,7 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError {
while true { while true {
token :: try consume(state, .ident) token :: try consume(state, .ident)
try arraylist.append(&names, token.span) 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) _ = try consume(state, .comma)
} }
@@ -169,13 +169,11 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError {
@hide:file @hide:file
parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { 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 ( if (
next_is(state, .ident) next_is(state, .ident)
and ( and (
next_n_is(state, 1, .ident) 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, .double_colon)
or next_n_is(state, 1, .colon_equal) 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) .for: return try parse_for(state, allow_else)
.return: return try parse_return(state, allow_else) .return: return try parse_return(state, allow_else)
.break, .continue: return try parse_loop_control(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) 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) return try ast.add_expression_stmt(&state.builder, expression)
} }
else: return .unexpected_token else: return .unexpected_token
@@ -239,9 +238,7 @@ parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseErr
} }
@hide:file @hide:file
parse_assignment proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { parse_assignment proc(state @mut State, target ast.NodeId, allow_else bool) ast.NodeId ! ParseError {
name :: try consume(state, .ident)
target :: try ast.add_identifier(&state.builder, name.span)
_ = try consume(state, .equal) _ = try consume(state, .equal)
value :: try parse_expr(state) value :: try parse_expr(state)
if ( 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 end :: state.tokens[state.next_token - 1].span.end
return try ast.add_assignment_stmt( return try ast.add_assignment_stmt(
&state.builder, &state.builder,
ast.Span{ start = name.span.start, end = end }, ast.Span{ start = ast.builder_span(&state.builder, target).start, end = end },
target, target,
value, value,
) )
@@ -454,8 +451,8 @@ parse_arm proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
.unexpected_token: {} .unexpected_token: {}
else: return err else: return err
} }
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) diagnostic :: try unexpected_diagnostic(state)
span :: synchronize_arm(state, start) span :: synchronize(state, start, true, true, false)
yield try ast.add_invalid_stmt(&state.builder, span, diagnostic) yield try ast.add_invalid_stmt(&state.builder, span, diagnostic)
} }
statements [1]ast.NodeId :: [statement] 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 @hide:file
parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError { parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
name :: try consume(state, .ident) 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) try parse_type(state)
else else
null null
@@ -508,9 +488,29 @@ parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
@hide:file @hide:file
parse_type proc(state @mut State) ast.NodeId ! ParseError { parse_type proc(state @mut State) ast.NodeId ! ParseError {
if (!next_is(state, .ident)) return .unexpected_token if next_is(state, .open_bracket) {
token :: current(state) 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 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) 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) skip_continuation_newlines(state)
token = current(state) token = current(state)
if (token.kind == .open_paren) { 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 left_grouped = false
continue continue
} }
@@ -603,7 +608,7 @@ validate_range_bound proc(
} }
@hide:file @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) _ = try consume(state, .open_paren)
state.paren_depth += 1 state.paren_depth += 1
defer 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( return try ast.add_call(
&state.builder, &state.builder,
ast.Span{ ast.Span{
start = ast.builder_span(&state.builder, callee).start, start = start,
end = close.span.end, end = close.span.end,
}, },
callee, 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 @hide:file
parse_primary proc(state @mut State) ast.NodeId ! ParseError { parse_primary proc(state @mut State) ast.NodeId ! ParseError {
token :: current(state) token :: current(state)
match token.kind { match token.kind {
.open_bracket: {
if array_starts_type(state) return try parse_type(state)
return try parse_array_literal(state)
}
.open_paren: { .open_paren: {
state.next_token += 1 state.next_token += 1
state.paren_depth += 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 { is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
return ( return (
kind == .comma kind == .comma
or kind == .close_bracket
or kind == .and or kind == .and
or kind == .or or kind == .or
or kind == .pipe 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 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. # a missing operand must not consume the declaration or assignment on the next line.
if ( if (
next_is(state, .ident) named_binding_starts(state)
and ( or (next_is(state, .ident) and next_n_is(state, 1, .equal))
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)
)
) state.next_token = start ) state.next_token = start
} }
@@ -744,6 +842,8 @@ skip_continuation_newlines proc(state @mut State) void {
kind :: current(state).kind kind :: current(state).kind
if ( if (
kind != .close_paren and kind != .comma and kind != .open_paren 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 and infix_binding_power(kind) == null
) state.next_token = start ) state.next_token = start
} }
@@ -753,8 +853,8 @@ recover_decl proc(
state @mut State, state @mut State,
start usize, start usize,
) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) { ) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) {
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) diagnostic :: try unexpected_diagnostic(state)
span :: synchronize(state, start, false) span :: synchronize(state, start, false, false, true)
return try ast.add_invalid_decl(&state.builder, span, diagnostic) return try ast.add_invalid_decl(&state.builder, span, diagnostic)
} }
@@ -763,31 +863,91 @@ recover_statement proc(
state @mut State, state @mut State,
start usize, start usize,
) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) { ) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) {
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) diagnostic :: try unexpected_diagnostic(state)
span :: synchronize(state, start, true) span :: synchronize(state, start, true, false, true)
return try ast.add_invalid_stmt(&state.builder, span, diagnostic) return try ast.add_invalid_stmt(&state.builder, span, diagnostic)
} }
@hide:file @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 first :: state.tokens[start].span
end := first.end 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) { while !next_is(state, .eof) {
if (depth == 0 and next_is(state, .newline)) break if (curly == 0) {
if (stop_at_curly and depth == 0 and next_is(state, .close_curly)) break if (stop_at_curly and next_is(state, .close_curly)) break
if (stop_at_curly and next_is(state, .open_curly)) depth += 1 if (allow_else and next_is(state, .else)) break
if (stop_at_curly and next_is(state, .close_curly) and depth > 0) depth -= 1 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 end = current(state).span.end
state.next_token += 1 state.next_token += 1
} }
if next_is(state, .newline) { if (consume_newline and next_is(state, .newline)) {
end = current(state).span.end end = current(state).span.end
state.next_token += 1 state.next_token += 1
} }
return ast.Span{ start = first.start, end = end } 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 @hide:file
skip_newlines proc(state @mut State) void { skip_newlines proc(state @mut State) void {
while next_is(state, .newline) state.next_token += 1 while next_is(state, .newline) state.next_token += 1
+198
View File
@@ -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(ast.ExprKind.call, ast.expr_kind(&tree, call_loop.iterable))
try testing.expect_equal(1, ast.block_statement_count(&tree, call_loop.body)) 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))
}
+569 -66
View File
@@ -15,21 +15,35 @@ TypeKind :: enum {
void, void,
function, function,
range, range,
array,
usize,
}
ArrayId :: distinct u32
no_array :: maxval!(ArrayId)
ArrayType :: struct {
element Type
len usize
mutable bool
} }
Type :: struct { Type :: struct {
kind TypeKind = .none kind TypeKind = .none
function ast.NodeId = ast.NodeId(maxval!(u32)) function ast.NodeId = ast.NodeId(maxval!(u32))
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
array_type ArrayId = no_array
range_element TypeKind = .none
} }
@hide:file @hide:file
Storage :: struct { Storage :: struct {
types []mut Type types []mut Type
arrays arraylist.ArrayList(ArrayType)
allocator mem.Allocator 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. #! function IDs refer to the caller-owned AST, not checker-owned signatures.
Result :: struct { storage Storage } Result :: struct { storage Storage }
@@ -68,6 +82,13 @@ Cell :: struct {
local bool = false local bool = false
assignment_value bool = false assignment_value bool = false
for_iterable 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 first_edge usize = no_index
return_annotation ast.NodeId = ast.NodeId(maxval!(u32)) return_annotation ast.NodeId = ast.NodeId(maxval!(u32))
} }
@@ -86,12 +107,13 @@ State :: struct {
types []mut Type types []mut Type
cells []mut Cell cells []mut Cell
edges arraylist.ArrayList(Edge) edges arraylist.ArrayList(Edge)
arrays arraylist.ArrayList(ArrayType)
collecting_assignment bool = false collecting_assignment bool = false
has_assignments bool = false has_assignments bool = false
} }
#! borrows the AST, resolution, and diagnostic store only during checking. #! 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( check proc(
allocator mem.Allocator, allocator mem.Allocator,
tree @ast.Tree, tree @ast.Tree,
@@ -115,9 +137,15 @@ check proc(
types = types, types = types,
cells = cells, cells = cells,
edges = arraylist.init(allocator), edges = arraylist.init(allocator),
arrays = arraylist.init(allocator),
} }
defer arraylist.deinit(&state.edges) 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| { for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i) id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) { match ast.decl_kind(tree, id) {
@@ -126,19 +154,21 @@ check proc(
.invalid: collect_invalid(&state, id) .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 if (cells[i].role != .assignment_stmt) continue
target :: ast.assignment_stmt(tree, ast.NodeId(i)).target target :: ast.assignment_stmt(tree, ast.NodeId(i)).target
if (ast.expr_kind(tree, target) != .identifier) continue
binding :: sema.binding(resolution, target) binding :: sema.binding(resolution, target)
if (binding.kind == .invalid) { if (binding.kind == .invalid) {
cells[i].type = invalid_type(binding.data.diagnostic) cells[i].type = invalid_type(binding.data.diagnostic)
continue } else if (binding.kind == .declaration and !mutable_target(&state, target)) {
}
if (binding.kind != .declaration or mutable_target(&state, target)) continue
cells[i].type = invalid_type(try diagnostics.add( cells[i].type = invalid_type(try diagnostics.add(
diagnostic_store, .invalid_assignment_target, ast.node_span(tree, target), diagnostic_store, .invalid_assignment_target, ast.node_span(tree, target),
)) ))
} }
}
# aliases share a type variable; explicit annotations and parameters are boundaries. # aliases share a type variable; explicit annotations and parameters are boundaries.
for (0..count) |i| { for (0..count) |i| {
@@ -175,13 +205,12 @@ check proc(
if (binding.kind == .declaration) cells[i].type = types[usize(binding.data.declaration)] if (binding.kind == .declaration) cells[i].type = types[usize(binding.data.declaration)]
} }
} }
# function aliases are known before calls; scalar return annotations # Calls use the signature result before contextual literal inference.
# mean a call cannot introduce a function type.
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(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 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 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))) { if (signature.function != ast.NodeId(maxval!(u32))) {
result :: ast.function_decl(tree, signature.function).return_type result :: ast.function_decl(tree, signature.function).return_type
seed(&state, id, types[usize(result)]) seed(&state, id, types[usize(result)])
@@ -209,6 +238,9 @@ check proc(
types[i] = cells[root(&state, i)].type types[i] = cells[root(&state, i)].type
} }
# reject nonnumeric operations before connecting otherwise valid inputs. # 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| { for (0..count) |i| {
id :: ast.NodeId(i) id :: ast.NodeId(i)
if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue if (cells[i].role != .expression or cells[i].assignment_value != assignment_values) continue
@@ -253,42 +285,13 @@ check proc(
else: {} 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) id :: ast.NodeId(i)
if (!assignment_values and cells[i].role == .declaration) { if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .index) continue
declaration :: ast.statement_decl(tree, id) try demand(&state, ast.index_expr(tree, id).index, Type{ kind = .usize }, .invalid_index_type)
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)
} }
# defaults follow ordinary program demands, then isolated assignment demands. # 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 (root(&state, i) != i or cells[i].assignment_value != assignment_values) continue
if (cells[i].type.kind == .invalid) continue if (cells[i].type.kind == .invalid) continue
if (cells[i].numeric and cells[i].anchored) { 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 } cells[i].type = Type{ kind = kind }
} }
} }
try prepare_shapes(&state, assignment_values, true)
if (!assignment_values) try specialize_annotations(&state)
for (0..count) |i| { for (0..count) |i| {
id :: ast.NodeId(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 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) 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. #! 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)] 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 { deinit proc(result @mut Result) void {
mem.free(result.storage.allocator, result.storage.types) mem.free(result.storage.allocator, result.storage.types)
arraylist.deinit(&result.storage.arrays)
} }
@hide:file @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].numeric = state.cells[a].numeric or state.cells[b].numeric
state.cells[a].anchored = state.cells[a].anchored or state.cells[b].anchored 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].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 @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) { 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)] if (state.cells[usize(id)].role == .annotation) return state.types[usize(id)]
state.cells[usize(id)].role = .annotation 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) binding :: sema.binding(state.resolution, id)
type := Type{} type := Type{}
match binding.kind { match binding.kind {
.invalid: type = invalid_type(binding.data.diagnostic) .invalid: type = invalid_type(binding.data.diagnostic)
.builtin: { .builtin: {
type.kind = match binding.data.builtin { type.kind = match binding.data.builtin {
.int: .int .int, .i64: .int
.float: .float .float, .f64: .float
.usize: .usize
.bool: .bool .bool: .bool
.string: .string .string: .string
.void: .void .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) 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) if (loop.index) |index| try collect_capture(state, index, loop.body)
try collect_block(state, loop.body, return_annotation, true) try collect_block(state, loop.body, return_annotation, true)
try depend(state, loop.body, id) try depend(state, loop.body, id)
@@ -686,6 +739,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
} }
.unary: { .unary: {
expression :: ast.unary_expr(tree, id) expression :: ast.unary_expr(tree, id)
if (expression.operator == .negate) state.cells[usize(id)].producer = id
if (expression.operator == .logical_not) { if (expression.operator == .logical_not) {
state.cells[usize(id)].type = Type{ kind = .bool } state.cells[usize(id)].type = Type{ kind = .bool }
} else { } else {
@@ -697,6 +751,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
} }
.binary: { .binary: {
expression :: ast.binary_expr(tree, id) 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)) { if (comparison(expression.operator) or logical(expression.operator)) {
state.cells[usize(id)].type = Type{ kind = .bool } state.cells[usize(id)].type = Type{ kind = .bool }
} else { } else {
@@ -731,13 +786,31 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
try depend(state, argument, id) 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) .invalid: collect_invalid(state, id)
} }
} }
@hide:file @hide:file
numeric_kind proc(kind TypeKind) bool { numeric_kind proc(kind TypeKind) bool {
return kind == .int or kind == .float return kind == .int or kind == .float or kind == .usize
} }
@hide:file @hide:file
@@ -819,23 +892,43 @@ numeric_constraint proc(state @mut State, expression, operand, operation ast.Nod
@hide:file @hide:file
demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code) void ! (mem.AllocError | diagnostics.CapacityError) { 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)) 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 if (!state.cells[index].numeric and state.cells[index].type.kind != .none) return
state.cells[index].numeric = true state.cells[index].numeric = true
try impose(state, id, expected.kind, code, ast.node_span(state.tree, id)) try impose(state, id, expected.kind, code, ast.node_span(state.tree, id))
} }
@hide:file @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 if (state.types[usize(id)].kind == .invalid) return
index :: root(state, usize(id)) index :: root(state, usize(id))
if (state.cells[index].type.kind == .invalid) return 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)) try fail(state, id, .invalid_range_bound, ast.node_span(state.tree, id))
return return
} }
try demand(state, id, Type{ kind = .int }, .invalid_range_bound) try demand(state, id, Type{ kind = family }, .invalid_range_bound)
} }
@hide:file @hide:file
@@ -854,6 +947,11 @@ require_bool proc(state @mut State, operand ast.NodeId) void ! (mem.AllocError |
@hide:file @hide:file
mutable_target proc(state @State, id ast.NodeId) bool { 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) binding :: sema.binding(state.resolution, id)
if (binding.kind != .declaration) return false if (binding.kind != .declaration) return false
target :: binding.data.declaration 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 if (ast.function_param_count(tree, left) != ast.function_param_count(tree, right)) return false
left_result :: ast.function_decl(tree, left).return_type left_result :: ast.function_decl(tree, left).return_type
right_result :: ast.function_decl(tree, right).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| { for (0..ast.function_param_count(tree, left)) |i| {
left_param :: ast.function_param(tree, left, i) left_param :: ast.function_param(tree, left, i)
right_param :: ast.function_param(tree, right, 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 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) { compatible proc(state @mut State, value ast.NodeId, expected Type) void ! (mem.AllocError | diagnostics.CapacityError) {
actual :: state.types[usize(value)] actual :: state.types[usize(value)]
if (actual.kind == .invalid or expected.kind == .invalid) return if (actual.kind == .invalid or expected.kind == .invalid) return
if (actual.kind != expected.kind) { if (!same_type(state, actual, expected)) {
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)) {
try fail(state, value, .type_mismatch, ast.node_span(state.tree, value)) 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_stmt: {
assignment :: ast.assignment_stmt(tree, id) 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)) { 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: { .return_stmt: {
@@ -912,7 +1014,7 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
actual :: state.types[usize(value)] actual :: state.types[usize(value)]
if (actual.kind == .invalid) { if (actual.kind == .invalid) {
state.types[usize(id)] = actual 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)) try fail(state, value, .return_type_mismatch, ast.node_span(tree, value))
state.types[usize(id)] = state.types[usize(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 return
} }
operand :: state.types[usize(expression.operand)] 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)) 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 and left.kind == .bool and right.kind == .bool) return
if (!numeric_kind(left.kind) or !numeric_kind(right.kind) or left.kind != right.kind) { if (!numeric_kind(left.kind) or !numeric_kind(right.kind) or left.kind != right.kind) {
try fail(state, id, .invalid_operand, expression.span) 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) 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) expression :: ast.range_expr(tree, id)
start_type :: state.types[usize(expression.start)] start_type :: state.types[usize(expression.start)]
end_type :: state.types[usize(expression.end)] 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)) 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)) 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) .call: try validate_call(state, id)
else: {} else: {}
} }
@@ -1017,3 +1145,378 @@ block_falls_through proc(state @State, id ast.NodeId) bool {
} }
return true 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)
}
}
}
+471
View File
@@ -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 expect_kind(&result, ast.block_statement(&tree, sibling_loop.body, 0), .int)
try testing.expect_equal(previous_count, diagnostics.count(&diagnostic_store)) 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))
}
+24 -1
View File
@@ -6,7 +6,12 @@ import "@std/strmap"
import "@source/ast" import "@source/ast"
import "@source/diagnostics" import "@source/diagnostics"
BuiltinType :: enum { int, float, bool, string, void } BuiltinType :: enum {
int, float,
bool, string,
void, i64,
f64, usize,
}
@hide:file @hide:file
builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([ builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([
@@ -15,6 +20,9 @@ builtins strmap.StaticStringMap(BuiltinType) :: strmap.init([
{ "bool", .bool }, { "bool", .bool },
{ "string", .string }, { "string", .string },
{ "void", .void }, { "void", .void },
{ "i64", .i64 },
{ "f64", .f64 },
{ "usize", .usize },
]) ])
BindingKind :: enum { none, declaration, builtin, invalid } BindingKind :: enum { none, declaration, builtin, invalid }
@@ -299,6 +307,21 @@ resolve_expr proc(
try resolve_expr(state, scope, ast.call_arg(tree, id, i)) 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) .invalid: resolve_invalid(state, id)
} }
} }
+152
View File
@@ -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(BindingKind.invalid, binding(&resolution, bounds.end).kind)
try testing.expect_equal(8, diagnostics.count(&diagnostic_store)) 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)
}