feat: execute Bro-compatible integer range loops
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Honey
|
# Honey
|
||||||
|
|
||||||
Honey is currently bootstrapped with the Bro compiler. the expected development workspace places the Bro project at [`../brolang`](../brolang); consult that project for compiler behavior and documentation.
|
Bro and Honey implement the same language; Bro is the prototype compiler used to bootstrap the self-hosted Honey compiler. the features below describe Honey's currently implemented subset. the expected development workspace places Bro at [`../brolang`](../brolang); consult that project for the language grammar and documentation.
|
||||||
|
|
||||||
style: see the [language guidelines](docs/style.md).
|
style: see the [language guidelines](docs/style.md).
|
||||||
|
|
||||||
@@ -51,7 +51,11 @@ function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. ca
|
|||||||
|
|
||||||
`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.
|
||||||
|
|
||||||
`break` exits the nearest enclosing `while`; `continue` restarts its entire condition, including short-circuit expressions and calls. 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`.
|
`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.
|
||||||
|
|
||||||
|
range bounds that are compound or unary expressions require their own parentheses, such as `(-2)..(limit + 1)`. a braceless body also requires parentheses around the entire iterable: `for (0..10) |i| total = total + i`. current execution supports direct integer ranges only; first-class range values, collection iteration, pointer captures and index captures remain unimplemented. [programs/range_loop.hon](programs/range_loop.hon) skips `5` while summing `0..10`; `./build/honey run programs/range_loop.hon` prints `40`.
|
||||||
|
|
||||||
|
`break` exits the nearest enclosing `for` or `while`; `continue` restarts a `while` condition or advances a `for` range through its guarded update. both are operand-free statements permitted within loop bodies and nested conditional arms; labels and values are unsupported. controls outside loops produce keyword-spanned diagnostics, including in untaken or unreachable code, and trap only if reached. code after a transfer is still resolved and type-checked but does not execute on that path. body locals are reset on re-entry after `continue`; loop transfers do not guarantee a function return. both controls lower to existing IR jumps, without new interpreter instructions or loop frames. [programs/loop_control.hon](programs/loop_control.hon) skips an iteration and exits early; `./build/honey run programs/loop_control.hon` prints `12`.
|
||||||
|
|
||||||
name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `bool`, `string`, `void`); sibling scopes may independently reuse names.
|
name resolution (`@source/sema`) runs after parsing. it predeclares package names and each block's direct locals, allowing forward references and mutually recursive functions. parameters and top-level function-body locals share a scope; each conditional arm and loop body introduces a child scope whose names do not escape it. signatures resolve names in the enclosing package scope. shadowing is forbidden, including reuse of builtin type names (`int`, `float`, `bool`, `string`, `void`); sibling scopes may independently reuse names.
|
||||||
|
|
||||||
@@ -67,4 +71,4 @@ type errors carry source spans and diagnostic identities; invalid operands propa
|
|||||||
|
|
||||||
`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.
|
`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.
|
||||||
|
|
||||||
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, `for` 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.
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
main proc() int {
|
||||||
|
total := 0
|
||||||
|
|
||||||
|
for 0..10 |i| {
|
||||||
|
if (i == 5) continue
|
||||||
|
total = total + i
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
+85
-4
@@ -3,13 +3,13 @@
|
|||||||
#! * 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 ..... -> identifier | literal | '(' expr ')' | ('-' | '!') expr | expr op 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
|
#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm
|
||||||
#! braceless guards require parentheses unless the condition is a call; 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.
|
#! precedence (tightest first): calls, 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"
|
||||||
@@ -39,6 +39,7 @@ ExprKind :: enum {
|
|||||||
unary
|
unary
|
||||||
binary
|
binary
|
||||||
call
|
call
|
||||||
|
range
|
||||||
invalid
|
invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ StmtKind :: enum {
|
|||||||
continue_stmt,
|
continue_stmt,
|
||||||
if_stmt,
|
if_stmt,
|
||||||
while_stmt,
|
while_stmt,
|
||||||
|
for_stmt,
|
||||||
invalid
|
invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +85,7 @@ NodeKind :: enum {
|
|||||||
expr_unary,
|
expr_unary,
|
||||||
expr_binary,
|
expr_binary,
|
||||||
expr_call,
|
expr_call,
|
||||||
|
expr_range,
|
||||||
stmt_expr,
|
stmt_expr,
|
||||||
stmt_decl,
|
stmt_decl,
|
||||||
stmt_return,
|
stmt_return,
|
||||||
@@ -91,6 +94,7 @@ NodeKind :: enum {
|
|||||||
stmt_if,
|
stmt_if,
|
||||||
stmt_assignment,
|
stmt_assignment,
|
||||||
stmt_while,
|
stmt_while,
|
||||||
|
stmt_for,
|
||||||
func_decl,
|
func_decl,
|
||||||
param_decl,
|
param_decl,
|
||||||
block,
|
block,
|
||||||
@@ -201,6 +205,23 @@ WhileView :: struct {
|
|||||||
body NodeId
|
body NodeId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RangeView :: struct {
|
||||||
|
span Span
|
||||||
|
start NodeId
|
||||||
|
end NodeId
|
||||||
|
inclusive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
ForView :: struct {
|
||||||
|
span Span
|
||||||
|
iterable NodeId
|
||||||
|
capture ?NodeId
|
||||||
|
index ?NodeId
|
||||||
|
pointer_capture bool
|
||||||
|
index_capture bool
|
||||||
|
body NodeId
|
||||||
|
}
|
||||||
|
|
||||||
InvalidView :: struct {
|
InvalidView :: struct {
|
||||||
span Span
|
span Span
|
||||||
diagnostic DiagnosticId
|
diagnostic DiagnosticId
|
||||||
@@ -287,6 +308,18 @@ add_binary proc(
|
|||||||
}, span)
|
}, span)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
add_range proc(
|
||||||
|
builder @mut Builder, span Span, start NodeId, end NodeId, inclusive bool,
|
||||||
|
) NodeId ! mem.AllocError {
|
||||||
|
flag :: if (inclusive) u32(1) else u32(0)
|
||||||
|
return try add_node(builder, Node{
|
||||||
|
kind = .expr_range,
|
||||||
|
main = NodeData{ extra = flag },
|
||||||
|
data0 = NodeData{ node_id = start },
|
||||||
|
data1 = NodeData{ node_id = end },
|
||||||
|
}, span)
|
||||||
|
}
|
||||||
|
|
||||||
add_call proc(
|
add_call proc(
|
||||||
builder @mut Builder,
|
builder @mut Builder,
|
||||||
span Span,
|
span Span,
|
||||||
@@ -368,6 +401,26 @@ add_while_stmt proc(
|
|||||||
}, span)
|
}, span)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
add_for_stmt proc(
|
||||||
|
builder @mut Builder, span Span, iterable NodeId, capture, index ?NodeId,
|
||||||
|
pointer_capture, index_capture bool, body NodeId,
|
||||||
|
) NodeId ! mem.AllocError {
|
||||||
|
raw_capture :: if (capture) |id| u32(id) else u32(no_node)
|
||||||
|
raw_index :: if (index) |id| u32(id) else u32(no_node)
|
||||||
|
pointer_flag :: if (pointer_capture) u32(1) else u32(0)
|
||||||
|
index_flag :: if (index_capture) u32(1) else u32(0)
|
||||||
|
extra :: try add_extra(builder, raw_capture)
|
||||||
|
_ = try add_extra(builder, raw_index)
|
||||||
|
_ = try add_extra(builder, pointer_flag)
|
||||||
|
_ = try add_extra(builder, index_flag)
|
||||||
|
return try add_node(builder, Node{
|
||||||
|
kind = .stmt_for,
|
||||||
|
main = NodeData{ node_id = iterable },
|
||||||
|
data0 = NodeData{ node_id = body },
|
||||||
|
data1 = NodeData{ extra = extra },
|
||||||
|
}, span)
|
||||||
|
}
|
||||||
|
|
||||||
add_statement_decl proc(
|
add_statement_decl proc(
|
||||||
builder @mut Builder,
|
builder @mut Builder,
|
||||||
span Span,
|
span Span,
|
||||||
@@ -508,6 +561,7 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
|
|||||||
.stmt_if: .if_stmt
|
.stmt_if: .if_stmt
|
||||||
.stmt_assignment: .assignment_stmt
|
.stmt_assignment: .assignment_stmt
|
||||||
.stmt_while: .while_stmt
|
.stmt_while: .while_stmt
|
||||||
|
.stmt_for: .for_stmt
|
||||||
.invalid_stmt: .invalid
|
.invalid_stmt: .invalid
|
||||||
else: unreachable
|
else: unreachable
|
||||||
}
|
}
|
||||||
@@ -559,6 +613,21 @@ while_stmt proc(tree @Tree, id NodeId) WhileView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for_stmt proc(tree @Tree, id NodeId) ForView {
|
||||||
|
node :: get_node(&tree.storage, id)
|
||||||
|
debug.assert(node.kind == .stmt_for)
|
||||||
|
extra :: usize(node.data1.extra)
|
||||||
|
return ForView{
|
||||||
|
span = get_span(&tree.storage, id),
|
||||||
|
iterable = node.main.node_id,
|
||||||
|
capture = maybe_node_id(tree.storage.extra.items[extra]),
|
||||||
|
index = maybe_node_id(tree.storage.extra.items[extra + 1]),
|
||||||
|
pointer_capture = tree.storage.extra.items[extra + 2] != 0,
|
||||||
|
index_capture = tree.storage.extra.items[extra + 3] != 0,
|
||||||
|
body = node.data0.node_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
statement_decl proc(tree @Tree, id NodeId) StatementDeclView {
|
statement_decl proc(tree @Tree, id NodeId) StatementDeclView {
|
||||||
node :: get_node(&tree.storage, id)
|
node :: get_node(&tree.storage, id)
|
||||||
debug.assert(node.kind == .stmt_decl)
|
debug.assert(node.kind == .stmt_decl)
|
||||||
@@ -640,6 +709,7 @@ expression_kind proc(kind NodeKind) ExprKind {
|
|||||||
.expr_unary: .unary
|
.expr_unary: .unary
|
||||||
.expr_binary: .binary
|
.expr_binary: .binary
|
||||||
.expr_call: .call
|
.expr_call: .call
|
||||||
|
.expr_range: .range
|
||||||
.invalid_expr: .invalid
|
.invalid_expr: .invalid
|
||||||
else: unreachable
|
else: unreachable
|
||||||
}
|
}
|
||||||
@@ -666,6 +736,17 @@ binary_expr proc(tree @Tree, id NodeId) BinaryExprView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
range_expr proc(tree @Tree, id NodeId) RangeView {
|
||||||
|
node :: get_node(&tree.storage, id)
|
||||||
|
debug.assert(node.kind == .expr_range)
|
||||||
|
return RangeView{
|
||||||
|
span = get_span(&tree.storage, id),
|
||||||
|
start = node.data0.node_id,
|
||||||
|
end = node.data1.node_id,
|
||||||
|
inclusive = node.main.extra != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
call_expr proc(tree @Tree, id NodeId) CallExprView {
|
call_expr proc(tree @Tree, id NodeId) CallExprView {
|
||||||
node :: get_node(&tree.storage, id)
|
node :: get_node(&tree.storage, id)
|
||||||
debug.assert(node.kind == .expr_call)
|
debug.assert(node.kind == .expr_call)
|
||||||
|
|||||||
@@ -94,6 +94,28 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept
|
|||||||
try append_text(renderer, "\n")
|
try append_text(renderer, "\n")
|
||||||
try render_block(renderer, tree, loop.body, depth + 1)
|
try render_block(renderer, tree, loop.body, depth + 1)
|
||||||
}
|
}
|
||||||
|
.for_stmt: {
|
||||||
|
loop :: ast.for_stmt(tree, statement)
|
||||||
|
try append_text(renderer, "for ")
|
||||||
|
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, loop.iterable)))
|
||||||
|
try append_text(renderer, " |")
|
||||||
|
if (loop.pointer_capture) try append_text(renderer, "@")
|
||||||
|
if (loop.capture) |capture| {
|
||||||
|
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, capture)))
|
||||||
|
} else {
|
||||||
|
try append_text(renderer, "_")
|
||||||
|
}
|
||||||
|
if (loop.index_capture) {
|
||||||
|
try append_text(renderer, ", ")
|
||||||
|
if (loop.index) |index| {
|
||||||
|
try append_text(renderer, ast.source_text(tree, ast.node_span(tree, index)))
|
||||||
|
} else {
|
||||||
|
try append_text(renderer, "_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try append_text(renderer, "|\n")
|
||||||
|
try render_block(renderer, tree, loop.body, depth + 1)
|
||||||
|
}
|
||||||
.invalid: try append_text(renderer, "invalid statement\n")
|
.invalid: try append_text(renderer, "invalid statement\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ Code :: enum {
|
|||||||
invalid_assignment_target,
|
invalid_assignment_target,
|
||||||
break_outside_loop,
|
break_outside_loop,
|
||||||
continue_outside_loop,
|
continue_outside_loop,
|
||||||
|
invalid_range_bound,
|
||||||
|
invalid_for_iterable,
|
||||||
|
range_value_not_supported,
|
||||||
|
range_pointer_capture,
|
||||||
|
range_index_capture,
|
||||||
missing_entry,
|
missing_entry,
|
||||||
invalid_entry,
|
invalid_entry,
|
||||||
literal_out_of_range,
|
literal_out_of_range,
|
||||||
@@ -102,6 +107,11 @@ messages :: $enummap.init(Code, []u8, {
|
|||||||
invalid_assignment_target = "assignment requires a mutable local binding",
|
invalid_assignment_target = "assignment requires a mutable local binding",
|
||||||
break_outside_loop = "break requires an enclosing loop",
|
break_outside_loop = "break requires an enclosing loop",
|
||||||
continue_outside_loop = "continue requires an enclosing loop",
|
continue_outside_loop = "continue requires an enclosing loop",
|
||||||
|
invalid_range_bound = "range bounds must have compatible integer types",
|
||||||
|
invalid_for_iterable = "for-loop iteration currently requires a direct integer range",
|
||||||
|
range_value_not_supported = "first-class range values are not implemented",
|
||||||
|
range_pointer_capture = "range loops do not support pointer captures",
|
||||||
|
range_index_capture = "range loops do not support index captures",
|
||||||
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",
|
||||||
|
|||||||
+18
-1
@@ -134,6 +134,18 @@ scan proc(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (char == '.' and cursor + 1 < program.len and program[cursor + 1] == '.') {
|
||||||
|
inclusive :: cursor + 2 < program.len and program[cursor + 2] == '='
|
||||||
|
width :: if (inclusive) 3 else 2
|
||||||
|
range_kind :: if (inclusive) .range_inclusive else .range
|
||||||
|
try add_token(state, Token{
|
||||||
|
span = make_span(cursor, cursor + width),
|
||||||
|
kind = range_kind,
|
||||||
|
})
|
||||||
|
cursor += width
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if char == ':' {
|
if char == ':' {
|
||||||
if cursor + 1 < program.len and program[cursor + 1] == ':' {
|
if cursor + 1 < program.len and program[cursor + 1] == ':' {
|
||||||
try add_token(state, Token{
|
try add_token(state, Token{
|
||||||
@@ -233,6 +245,8 @@ scan proc(
|
|||||||
'<': .less
|
'<': .less
|
||||||
'>': .greater
|
'>': .greater
|
||||||
',': .comma
|
',': .comma
|
||||||
|
'|': .pipe
|
||||||
|
'@': .at
|
||||||
'(': .open_paren
|
'(': .open_paren
|
||||||
')': .close_paren
|
')': .close_paren
|
||||||
'[': .open_bracket
|
'[': .open_bracket
|
||||||
@@ -304,7 +318,10 @@ scan_number proc(start usize, program []u8) ScanNumResult ! ScanError {
|
|||||||
cursor := start
|
cursor := start
|
||||||
has_decimal := false
|
has_decimal := false
|
||||||
while (cursor < program.len and is_digit(program[cursor])) cursor += 1
|
while (cursor < program.len and is_digit(program[cursor])) cursor += 1
|
||||||
if cursor < program.len and program[cursor] == '.' {
|
if (
|
||||||
|
cursor < program.len and program[cursor] == '.'
|
||||||
|
and !(cursor + 1 < program.len and program[cursor + 1] == '.')
|
||||||
|
) {
|
||||||
has_decimal = true
|
has_decimal = true
|
||||||
cursor += 1
|
cursor += 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ reports_invalid_input_and_keeps_scanning test {
|
|||||||
state := init(mem.c_allocator)
|
state := init(mem.c_allocator)
|
||||||
defer deinit(&state)
|
defer deinit(&state)
|
||||||
|
|
||||||
try scan(&state, "if name # comment\n@1. 2 3.5 \"hi\"", &diagnostic_store)
|
try scan(&state, "if name # comment\n$1. 2 3.5 \"hi\"", &diagnostic_store)
|
||||||
|
|
||||||
try testing.expect_equal(8, state.tokens.items.len)
|
try testing.expect_equal(8, state.tokens.items.len)
|
||||||
try testing.expect_equal(TokenKind.if, state.tokens.items[0].kind)
|
try testing.expect_equal(TokenKind.if, state.tokens.items[0].kind)
|
||||||
@@ -100,3 +100,25 @@ scans_loop_controls_with_exact_keyword_boundaries test {
|
|||||||
try testing.expect_equal("continued", strpool.get_str(&strpool.strings, state.tokens.items[3].data.str_id)?)
|
try testing.expect_equal("continued", strpool.get_str(&strpool.strings, state.tokens.items[3].data.str_id)?)
|
||||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scans_ranges_without_consuming_float_dots 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, "0..10 0..=10 1.5..2.5 |@item, _| ... 1.", &diagnostic_store)
|
||||||
|
expected [19]TokenKind :: [
|
||||||
|
.int, .range, .int, .int, .range_inclusive, .int,
|
||||||
|
.float, .range, .float, .pipe, .at, .ident, .comma, .ident, .pipe,
|
||||||
|
.range, .invalid, .invalid, .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(u32(1), state.tokens.items[1].span.start)
|
||||||
|
try testing.expect_equal(u32(3), state.tokens.items[1].span.end)
|
||||||
|
try testing.expect_equal(u32(7), state.tokens.items[4].span.start)
|
||||||
|
try testing.expect_equal(u32(10), state.tokens.items[4].span.end)
|
||||||
|
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ TokenKind :: enum {
|
|||||||
plus, minus
|
plus, minus
|
||||||
star, slash
|
star, slash
|
||||||
|
|
||||||
|
range, range_inclusive
|
||||||
|
pipe, at
|
||||||
|
|
||||||
# logical
|
# logical
|
||||||
bang
|
bang
|
||||||
|
|
||||||
|
|||||||
+84
-5
@@ -18,7 +18,7 @@ Storage :: struct {
|
|||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
LoopTargets :: struct {
|
LoopTargets :: struct {
|
||||||
guard ir.BlockId
|
continue_target ir.BlockId
|
||||||
exit ir.BlockId
|
exit ir.BlockId
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ lower_type proc(type typecheck.Type) ir.Type {
|
|||||||
.string: .string
|
.string: .string
|
||||||
.void: .void
|
.void: .void
|
||||||
.function: .function
|
.function: .function
|
||||||
.none, .invalid: .invalid
|
.none, .invalid, .range: .invalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,6 +267,19 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! mem.AllocError
|
|||||||
if (conditional.else_block) |alternative| try predeclare_locals(state, alternative)
|
if (conditional.else_block) |alternative| try predeclare_locals(state, alternative)
|
||||||
}
|
}
|
||||||
.while_stmt: try predeclare_locals(state, ast.while_stmt(state.tree, statement).body)
|
.while_stmt: try predeclare_locals(state, ast.while_stmt(state.tree, statement).body)
|
||||||
|
.for_stmt: {
|
||||||
|
loop :: ast.for_stmt(state.tree, statement)
|
||||||
|
function := &state.module.functions.items[state.function]
|
||||||
|
slot :: u32(function.locals.items.len)
|
||||||
|
try arraylist.append(&function.locals, ir.Type.int)
|
||||||
|
state.storage[usize(statement)] = Storage{ kind = .local, index = slot }
|
||||||
|
if (loop.capture) |capture| state.storage[usize(capture)] = state.storage[usize(statement)]
|
||||||
|
if (loop.index) |index| {
|
||||||
|
state.storage[usize(index)] = Storage{ kind = .local, index = u32(function.locals.items.len) }
|
||||||
|
try arraylist.append(&function.locals, ir.Type.int)
|
||||||
|
}
|
||||||
|
try predeclare_locals(state, loop.body)
|
||||||
|
}
|
||||||
else: {}
|
else: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +378,7 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn
|
|||||||
span = ast.node_span(state.tree, loop.condition),
|
span = ast.node_span(state.tree, loop.condition),
|
||||||
}
|
}
|
||||||
previous_loop :: state.loop
|
previous_loop :: state.loop
|
||||||
state.loop = LoopTargets{ guard = guard, exit = exit }
|
state.loop = LoopTargets{ continue_target = guard, exit = exit }
|
||||||
defer state.loop = previous_loop
|
defer state.loop = previous_loop
|
||||||
state.block = body
|
state.block = body
|
||||||
try lower_block(state, loop.body, true)
|
try lower_block(state, loop.body, true)
|
||||||
@@ -378,18 +391,80 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn
|
|||||||
state.terminated = false
|
state.terminated = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
loop :: ast.for_stmt(state.tree, id)
|
||||||
|
if (ast.expr_kind(state.tree, loop.iterable) != .range) {
|
||||||
|
_ = try lower_expression(state, loop.iterable)
|
||||||
|
if (!state.terminated) _ = trap_type(state, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
interval :: ast.range_expr(state.tree, loop.iterable)
|
||||||
|
start :: try lower_expression(state, interval.start)
|
||||||
|
if (state.terminated) return
|
||||||
|
end :: try lower_expression(state, interval.end)
|
||||||
|
if (state.terminated or trap_type(state, loop.iterable) or trap_type(state, id)) return
|
||||||
|
slot :: state.storage[usize(id)].index
|
||||||
|
_ = try emit(state, ir.Instruction{ op = .store_local, type = .int, left = start, slot = slot, span = interval.span })
|
||||||
|
origin :: state.block
|
||||||
|
condition :: try add_block(state)
|
||||||
|
body :: try add_block(state)
|
||||||
|
update :: try add_block(state)
|
||||||
|
increment :: try add_block(state)
|
||||||
|
exit :: try add_block(state)
|
||||||
|
state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{
|
||||||
|
kind = .jump, target = condition, span = loop.span,
|
||||||
|
}
|
||||||
|
state.block = condition
|
||||||
|
current :: try emit(state, ir.Instruction{ op = .load_local, type = .int, slot = slot, span = interval.span })
|
||||||
|
comparison ir.Op :: if (interval.inclusive) .less_equal else .less
|
||||||
|
condition_value :: try emit(state, ir.Instruction{ op = comparison, type = .bool, left = current, right = end, span = interval.span })
|
||||||
|
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
|
||||||
|
kind = .branch, value = condition_value, target = body, alternative = exit, span = interval.span,
|
||||||
|
}
|
||||||
|
previous_loop :: state.loop
|
||||||
|
state.loop = LoopTargets{ continue_target = update, exit = exit }
|
||||||
|
defer state.loop = previous_loop
|
||||||
|
state.block = body
|
||||||
|
if (loop.capture) |capture| _ = trap_binding(state, capture)
|
||||||
|
if (!state.terminated) try lower_block(state, loop.body, true)
|
||||||
|
if (!state.terminated) {
|
||||||
|
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
|
||||||
|
kind = .jump, target = update, span = loop.span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.block = update
|
||||||
|
state.terminated = false
|
||||||
|
last :: try emit(state, ir.Instruction{ op = .load_local, type = .int, slot = slot, span = interval.span })
|
||||||
|
finished :: try emit(state, ir.Instruction{ op = .equal, type = .bool, left = last, right = end, span = interval.span })
|
||||||
|
# continue reaches this guard too, so an inclusive maximum never adds one.
|
||||||
|
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
|
||||||
|
kind = .branch, value = finished, target = exit, alternative = increment, span = interval.span,
|
||||||
|
}
|
||||||
|
state.block = increment
|
||||||
|
one :: try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } }, interval.span)
|
||||||
|
next :: try emit(state, ir.Instruction{ op = .add, type = .int, left = last, right = one, span = interval.span })
|
||||||
|
_ = try emit(state, ir.Instruction{ op = .store_local, type = .int, left = next, slot = slot, span = interval.span })
|
||||||
|
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
|
||||||
|
kind = .jump, target = condition, span = loop.span,
|
||||||
|
}
|
||||||
|
state.block = exit
|
||||||
|
state.terminated = false
|
||||||
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
tree :: state.tree
|
tree :: state.tree
|
||||||
match ast.stmt_kind(tree, id) {
|
match ast.stmt_kind(tree, id) {
|
||||||
.if_stmt: try lower_if(state, id)
|
.if_stmt: try lower_if(state, id)
|
||||||
.while_stmt: try lower_while(state, id)
|
.while_stmt: try lower_while(state, id)
|
||||||
|
.for_stmt: try lower_for(state, id)
|
||||||
.break_stmt, .continue_stmt: {
|
.break_stmt, .continue_stmt: {
|
||||||
if (trap_type(state, id)) return
|
if (trap_type(state, id)) return
|
||||||
if (state.loop) |targets| {
|
if (state.loop) |targets| {
|
||||||
target := targets.exit
|
target := targets.exit
|
||||||
# continue restarts the whole guard, before any short-circuit branches.
|
# each loop supplies its own guard or guarded-update entry.
|
||||||
if (ast.stmt_kind(tree, id) == .continue_stmt) target = targets.guard
|
if (ast.stmt_kind(tree, id) == .continue_stmt) target = targets.continue_target
|
||||||
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 = target, span = ast.node_span(tree, id),
|
kind = .jump, target = target, span = ast.node_span(tree, id),
|
||||||
}
|
}
|
||||||
@@ -491,6 +566,10 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
|||||||
invalid :: ast.invalid(tree, id)
|
invalid :: ast.invalid(tree, id)
|
||||||
trap(state, invalid.diagnostic, invalid.span)
|
trap(state, invalid.diagnostic, invalid.span)
|
||||||
}
|
}
|
||||||
|
.range: {
|
||||||
|
# ranges have no physical value; only lower_for decomposes an iterable.
|
||||||
|
if (!trap_type(state, id)) unreachable
|
||||||
|
}
|
||||||
.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)
|
||||||
|
|||||||
@@ -439,6 +439,15 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
|
|||||||
`main proc() string {
|
`main proc() string {
|
||||||
` computed float :: factor * 2
|
` computed float :: factor * 2
|
||||||
` remaining := 2
|
` remaining := 2
|
||||||
|
` for 0..=2 |item| {
|
||||||
|
` saved :: identity(text)
|
||||||
|
` for 0..2 |_| {
|
||||||
|
` nested :: identity(saved)
|
||||||
|
` identity(nested)
|
||||||
|
` if (item == 1) continue else break
|
||||||
|
` }
|
||||||
|
` if (item < 2) continue else break
|
||||||
|
` }
|
||||||
` while compose(remaining > 0 and enabled, compose(!false, compose(true or false, compose(true, compose(true, true))))) {
|
` while compose(remaining > 0 and enabled, compose(!false, compose(true or false, compose(true, compose(true, true))))) {
|
||||||
` pass :: identity(text)
|
` pass :: identity(text)
|
||||||
` identity(pass)
|
` identity(pass)
|
||||||
@@ -1117,8 +1126,16 @@ lazy_branchful_global_initializers_and_calls_release_every_runtime_allocation te
|
|||||||
`first :: true and later
|
`first :: true and later
|
||||||
`later :: false or !false
|
`later :: false or !false
|
||||||
`both proc(left bool, right bool) bool { return left and right }
|
`both proc(left bool, right bool) bool { return left and right }
|
||||||
|
`bound proc(flag bool, value int) int {
|
||||||
|
` if flag { return value }
|
||||||
|
` return 0
|
||||||
|
`}
|
||||||
`main proc() bool {
|
`main proc() bool {
|
||||||
` n := 3
|
` n := 3
|
||||||
|
` for bound(first or false, 0)..bound(both(first, true and later), 3) |i| {
|
||||||
|
` local :: both(first and first, later or false)
|
||||||
|
` if (local and i < 2) continue else break
|
||||||
|
` }
|
||||||
` while both(first and first, both(false or later, !false and later)) and n > 0 {
|
` while both(first and first, both(false or later, !false and later)) and n > 0 {
|
||||||
` n = n - 1
|
` n = n - 1
|
||||||
` if (n > 0) continue else break
|
` if (n > 0) continue else break
|
||||||
@@ -1299,3 +1316,221 @@ traps_before_loop_controls_keep_the_original_error test {
|
|||||||
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
owned_integer_ranges_execute_exclusive_inclusive_empty_signed_and_discard_forms test {
|
||||||
|
sources [13][]u8 :: [
|
||||||
|
"main proc() int {\n sum := 0\n for 0..7 |i| { sum = sum + i }\n return sum\n}\n",
|
||||||
|
"main proc() int {\n sum := 0\n for (0..=6) |i| { sum = sum + i }\n return sum\n}\n",
|
||||||
|
"main proc() int {\n sum := 0\n for (-3)..3 |i| { sum = sum + i }\n return sum\n}\n",
|
||||||
|
"main proc() int {\n sum := 0\n for (-3)..=3 |i| { sum = sum + i }\n return sum\n}\n",
|
||||||
|
"main proc() int {\n sum := 0\n for 7..7 |i| { sum = sum + i }\n return sum\n}\n",
|
||||||
|
"main proc() int {\n sum := 0\n for 7..=7 |i| { sum = sum + i }\n return sum\n}\n",
|
||||||
|
"main proc() int {\n count := 0\n for 7..2 |_| { count = count + 1 }\n for 7..=2 |_| { count = count + 1 }\n return count\n}\n",
|
||||||
|
"main proc() int {\n count := 0\n for (0..4) |_| count = count + 1\n return count\n}\n",
|
||||||
|
"main proc() int {\n count := 0\n for 9223372036854775807..=9223372036854775807 |i| {\n count = count + 1\n if (count > 2) break\n continue\n }\n return count\n}\n",
|
||||||
|
"main proc() int {\n count := 0\n for 9223372036854775806..=9223372036854775807 |_| {\n count = count + 1\n if (count > 3) break\n continue\n }\n return count\n}\n",
|
||||||
|
"main proc() int {\n count := 0\n for 9223372036854775806..9223372036854775807 |i| {\n count = count + 1\n if (count > 2) break\n continue\n }\n return count\n}\n",
|
||||||
|
"main proc() int {\n for (-9223372036854775808)..(-9223372036854775807) |i| { return i }\n return 0\n}\n",
|
||||||
|
"main proc() int {\n sum := 0\n for 0..(3 + 1) |i| { sum = sum + i }\n return sum\n}\n",
|
||||||
|
]
|
||||||
|
expected [13]i64 :: [21, 21, -3, 0, 0, 7, 0, 4, 1, 2, 1, minval!(i64), 6]
|
||||||
|
for (sources) |source, index| {
|
||||||
|
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)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(ir.Type.int, outcome.value.type)
|
||||||
|
try testing.expect_equal(expected[index], outcome.value.data.integer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
range_bounds_calls_and_globals_are_frozen_before_branchful_body_calls test {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source ::
|
||||||
|
`seed :: choose(true or false, identity(0))
|
||||||
|
`limit :: choose(false or true, identity(4))
|
||||||
|
`identity proc(value int) int { return value }
|
||||||
|
`choose proc(flag bool, value int) int {
|
||||||
|
` if flag { return value }
|
||||||
|
` return 99
|
||||||
|
`}
|
||||||
|
`main proc() int {
|
||||||
|
` first := seed
|
||||||
|
` last := limit
|
||||||
|
` sum := 0
|
||||||
|
` for choose(true and !false, identity(first))..choose(false or true, choose(true or false, last)) |i| {
|
||||||
|
` sum = choose(i == 0 or i > 0, identity(sum + i))
|
||||||
|
` first = 100
|
||||||
|
` last = 0
|
||||||
|
` }
|
||||||
|
` return sum + 36
|
||||||
|
`}
|
||||||
|
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)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
range_break_continue_and_nested_for_while_restore_lexical_targets test {
|
||||||
|
sources [3][]u8 :: [
|
||||||
|
"main proc() int {\n sum := 0\n count := 0\n for 0..10 |i| {\n count = count + 1\n if (count > 12) break\n if (i == 2 or i == 4) continue\n if (i == 6) break\n sum = sum + i\n }\n return sum + 33\n}\n",
|
||||||
|
"main proc() int {\n sum := 0\n count := 0\n for 0..3 |outer| {\n count = count + 1\n if (count > 4) break\n inner := 3\n while inner > 0 {\n inner = inner - 1\n if (inner == 1) continue\n if (inner == 0) break\n sum = sum + 7\n }\n if (outer < 2) continue else break\n }\n return sum + 21\n}\n",
|
||||||
|
"main proc() int {\n outer := 3\n sum := 0\n while outer > 0 {\n outer = outer - 1\n count := 0\n for 0..3 |i| {\n count = count + 1\n if (count > 4) break\n for 0..3 |j| {\n if (j == 0) continue\n sum = sum + 7\n break\n }\n if (i == 0) continue else break\n }\n if (outer > 0) continue else break\n }\n return sum\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)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
range_body_and_arm_locals_reset_before_continued_later_declarations test {
|
||||||
|
sources [6][]u8 :: [
|
||||||
|
"main proc() int {\n for 0..3 |i| {\n if i == 2 { return later }\n if (i == 1) continue\n later :: 42\n continue\n }\n return 0\n}\n",
|
||||||
|
"main proc() int {\n for 0..3 |i| {\n if i == 2 { later = 42 }\n if (i == 1) continue\n later := 0\n continue\n }\n return 0\n}\n",
|
||||||
|
"main proc() int {\n for 0..3 |i| {\n if true {\n if i == 2 { return later }\n if (i == 1) continue\n later :: 42\n continue\n }\n }\n return 0\n}\n",
|
||||||
|
"main proc() int {\n for 0..3 |i| {\n if true {\n if i == 2 { later = 42 }\n if (i == 1) continue\n later := 0\n continue\n }\n }\n return 0\n}\n",
|
||||||
|
"main proc() int {\n for 0..3 |i| {\n if false {} else {\n if i == 2 { return later }\n if (i == 1) continue\n later :: 42\n continue\n }\n }\n return 0\n}\n",
|
||||||
|
"main proc() int {\n for 0..3 |i| {\n if false {} else {\n if i == 2 { later = 42 }\n if (i == 1) continue\n later := 0\n continue\n }\n }\n return 0\n}\n",
|
||||||
|
]
|
||||||
|
codes [6]diagnostics.Code :: [
|
||||||
|
.read_before_initialization, .write_before_initialization,
|
||||||
|
.read_before_initialization, .write_before_initialization,
|
||||||
|
.read_before_initialization, .write_before_initialization,
|
||||||
|
]
|
||||||
|
for (sources) |source, index| {
|
||||||
|
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)
|
||||||
|
try expect_trap(outcome, &diagnostic_store, codes[index])
|
||||||
|
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)?
|
||||||
|
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], "later"))
|
||||||
|
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
untaken_range_errors_and_errors_after_controls_remain_diagnostic_only test {
|
||||||
|
sources [10][]u8 :: [
|
||||||
|
"main proc() int {\n for 0..0 |i| { return unknown }\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 3..2 |i| { i = 1 }\n return 42\n}\n",
|
||||||
|
"main proc() int {\n i :: 0\n for 0..0 |i| {}\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..3 |i| {\n continue\n missing\n }\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..3 |i| {\n break\n missing\n }\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..3 |i| {\n return 42\n break\n continue\n missing\n }\n return 0\n}\n",
|
||||||
|
"unused proc() int {\n for 0..1 |i| { return unknown }\n return 0\n}\nmain proc() int { return 42 }\n",
|
||||||
|
"main proc() int {\n if false { for true..1 |i| {} }\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..0 |int| {}\n return 42\n}\n",
|
||||||
|
"unsupported :: 0..1\nmain proc() int {\n for 0..0 |_| { local :: 0..1 }\n return 42\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)
|
||||||
|
count :: diagnostics.count(&diagnostic_store)
|
||||||
|
try testing.expect(count > 0)
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||||
|
try testing.expect_equal(count, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
demanded_range_header_capture_and_body_errors_keep_original_diagnostic_identity test {
|
||||||
|
sources [10][]u8 :: [
|
||||||
|
"main proc() int {\n for true..1 |i| {}\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..1 |i| { i = 1 }\n return 42\n}\n",
|
||||||
|
"main proc() int {\n i :: 0\n for 0..1 |i| {}\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..1 |i| {\n missing\n continue\n }\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..1 |i| {\n missing\n break\n }\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 1 |i| {}\n return 42\n}\n",
|
||||||
|
"main proc() int {\n 0..1\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..1 |@i| {}\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..1 |i, _| {}\n return 42\n}\n",
|
||||||
|
"main proc() int {\n local :: 0..1\n return 42\n}\n",
|
||||||
|
]
|
||||||
|
codes [10]diagnostics.Code :: [
|
||||||
|
.invalid_range_bound, .invalid_assignment_target, .duplicate_name,
|
||||||
|
.unknown_name, .unknown_name, .invalid_for_iterable, .range_value_not_supported,
|
||||||
|
.range_pointer_capture, .range_index_capture, .range_value_not_supported,
|
||||||
|
]
|
||||||
|
for (sources) |source, index| {
|
||||||
|
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(1, diagnostics.count(&diagnostic_store))
|
||||||
|
original :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(0))?
|
||||||
|
try testing.expect_equal(codes[index], original.code)
|
||||||
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
|
||||||
|
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
range_bounds_evaluate_left_to_right_before_header_errors_and_body_overflow_keeps_span test {
|
||||||
|
sources [4][]u8 :: [
|
||||||
|
"first :: fail()\nsecond :: absent\nfail proc() int { return 9223372036854775807 + 1 }\nmain proc() int {\n for first..second |i| {}\n return 42\n}\n",
|
||||||
|
"fail proc() int { return 9223372036854775807 + 1 }\nmain proc() int {\n for fail()..1 |@i| {}\n return 42\n}\n",
|
||||||
|
"main proc() int {\n for 0..1 |i| { return 9223372036854775807 + i + 1 }\n return 0\n}\n",
|
||||||
|
"main proc() int {\n for first_absent..second_absent |i| {}\n return 42\n}\n",
|
||||||
|
]
|
||||||
|
for (sources) |source, index| {
|
||||||
|
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)
|
||||||
|
code diagnostics.Code :: if (index == 3) .unknown_name else .integer_overflow
|
||||||
|
try expect_trap(outcome, &diagnostic_store, code)
|
||||||
|
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)?
|
||||||
|
span []u8 :: match index {
|
||||||
|
0, 1: "9223372036854775807 + 1"
|
||||||
|
2: "9223372036854775807 + i + 1"
|
||||||
|
3: "first_absent"
|
||||||
|
else: unreachable
|
||||||
|
}
|
||||||
|
try testing.expect(mem.eql(source[usize(diagnostic.span.start)..usize(diagnostic.span.end)], span))
|
||||||
|
if (index == 3) try testing.expect_equal(diagnostics.DiagnosticId(0), outcome.trap?)
|
||||||
|
else try testing.expect_equal(diagnostics.DiagnosticId(before), outcome.trap?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
range_runtime_allocations_do_not_grow_with_iterations test {
|
||||||
|
sources [2][]u8 :: [
|
||||||
|
"main proc() int {\n count := 0\n for 0..1 |i| {\n local :: i\n count = count + 1\n if (count > 1) break\n if (local >= 0) continue\n later :: 0\n }\n return count\n}\n",
|
||||||
|
"main proc() int {\n count := 0\n for 0..10000 |i| {\n local :: i\n count = count + 1\n if (count > 10000) break\n if (local >= 0) continue\n later :: 0\n }\n return count\n}\n",
|
||||||
|
]
|
||||||
|
expected [2]i64 :: [1, 10000]
|
||||||
|
allocations := usize(0)
|
||||||
|
for (sources) |source, index| {
|
||||||
|
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)
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(expected[index], outcome.value.data.integer)
|
||||||
|
try testing.expect_equal(usize(0), state.live)
|
||||||
|
if (index == 0) allocations = usize(64) - state.remaining
|
||||||
|
else try testing.expect_equal(allocations, usize(64) - state.remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+127
-1
@@ -185,6 +185,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
|
|||||||
match current(state).kind {
|
match current(state).kind {
|
||||||
.if: return try parse_if(state)
|
.if: return try parse_if(state)
|
||||||
.while: return try parse_while(state, allow_else)
|
.while: return try parse_while(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: {
|
||||||
@@ -302,6 +303,92 @@ parse_while proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
CaptureList :: struct {
|
||||||
|
item ?ast.NodeId = null
|
||||||
|
index ?ast.NodeId = null
|
||||||
|
pointer bool = false
|
||||||
|
indexed bool = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
parse_capture_name proc(state @mut State) ?ast.NodeId ! ParseError {
|
||||||
|
name :: try consume(state, .ident)
|
||||||
|
capture ?ast.NodeId := null
|
||||||
|
if (
|
||||||
|
name.span.end != name.span.start + 1
|
||||||
|
or state.builder.source[usize(name.span.start)] != '_'
|
||||||
|
) capture = try ast.add_identifier(&state.builder, name.span)
|
||||||
|
return capture
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
parse_captures proc(state @mut State) CaptureList ! ParseError {
|
||||||
|
_ = try consume(state, .pipe)
|
||||||
|
captures := CaptureList{}
|
||||||
|
if next_is(state, .at) {
|
||||||
|
captures.pointer = true
|
||||||
|
state.next_token += 1
|
||||||
|
}
|
||||||
|
captures.item = try parse_capture_name(state)
|
||||||
|
if next_is(state, .comma) {
|
||||||
|
captures.indexed = true
|
||||||
|
state.next_token += 1
|
||||||
|
captures.index = try parse_capture_name(state)
|
||||||
|
}
|
||||||
|
_ = try consume(state, .pipe)
|
||||||
|
return captures
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
parse_for proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
||||||
|
keyword :: try consume(state, .for)
|
||||||
|
skip_newlines(state)
|
||||||
|
iterable_start :: state.next_token
|
||||||
|
iterable := try parse_expr(state)
|
||||||
|
grouped :: parenthesized_guard(state, iterable_start)
|
||||||
|
skip_newlines(state)
|
||||||
|
captures :: parse_captures(state) catch |err| {
|
||||||
|
match err {
|
||||||
|
.unexpected_token: {}
|
||||||
|
else: return err
|
||||||
|
}
|
||||||
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||||
|
iterable = try ast.add_invalid_expr(
|
||||||
|
&state.builder, ast.builder_span(&state.builder, iterable), diagnostic,
|
||||||
|
)
|
||||||
|
while (
|
||||||
|
!next_is(state, .open_curly) and !next_is(state, .close_curly)
|
||||||
|
and !next_is(state, .newline) and !next_is(state, .eof)
|
||||||
|
and !next_is(state, .else)
|
||||||
|
) state.next_token += 1
|
||||||
|
yield CaptureList{}
|
||||||
|
}
|
||||||
|
after_captures :: state.next_token
|
||||||
|
skip_newlines(state)
|
||||||
|
if (
|
||||||
|
!next_is(state, .open_curly) and !grouped
|
||||||
|
and ast.builder_expr_kind(&state.builder, iterable) != .call
|
||||||
|
and ast.builder_expr_kind(&state.builder, iterable) != .invalid
|
||||||
|
) {
|
||||||
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||||
|
iterable = try ast.add_invalid_expr(
|
||||||
|
&state.builder, ast.builder_span(&state.builder, iterable), diagnostic,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
# a malformed capture list must not swallow the next line's sibling.
|
||||||
|
if (
|
||||||
|
ast.builder_expr_kind(&state.builder, iterable) == .invalid
|
||||||
|
and !next_is(state, .open_curly)
|
||||||
|
) state.next_token = after_captures
|
||||||
|
body :: try parse_arm(state, allow_else)
|
||||||
|
return try ast.add_for_stmt(
|
||||||
|
&state.builder,
|
||||||
|
ast.Span{ start = keyword.span.start, end = ast.builder_span(&state.builder, body).end },
|
||||||
|
iterable, captures.item, captures.index, captures.pointer, captures.indexed, body,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
parse_guard proc(state @mut State) ast.NodeId ! ParseError {
|
parse_guard proc(state @mut State) ast.NodeId ! ParseError {
|
||||||
condition_start :: state.next_token
|
condition_start :: state.next_token
|
||||||
@@ -320,6 +407,7 @@ parse_guard proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
and !next_is(state, .newline) and !next_is(state, .eof)
|
and !next_is(state, .newline) and !next_is(state, .eof)
|
||||||
and !next_is(state, .else) and !next_is(state, .return)
|
and !next_is(state, .else) and !next_is(state, .return)
|
||||||
and !next_is(state, .if) and !next_is(state, .while)
|
and !next_is(state, .if) and !next_is(state, .while)
|
||||||
|
and !next_is(state, .for)
|
||||||
and !next_is(state, .break) and !next_is(state, .continue)
|
and !next_is(state, .break) and !next_is(state, .continue)
|
||||||
) {
|
) {
|
||||||
if (next_is(state, .ident) and next_n_is(state, 1, .equal)) break
|
if (next_is(state, .ident) and next_n_is(state, 1, .equal)) break
|
||||||
@@ -435,6 +523,7 @@ parse_expr proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! ParseError {
|
parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! ParseError {
|
||||||
skip_operand_newlines(state)
|
skip_operand_newlines(state)
|
||||||
token := current(state)
|
token := current(state)
|
||||||
|
expression_start :: state.next_token
|
||||||
left := if (prefix_binding_power(token.kind)) |right_bp| {
|
left := if (prefix_binding_power(token.kind)) |right_bp| {
|
||||||
state.next_token += 1
|
state.next_token += 1
|
||||||
operand :: try parse_expr_bp(state, right_bp)
|
operand :: try parse_expr_bp(state, right_bp)
|
||||||
@@ -449,12 +538,32 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
|
|||||||
} else {
|
} else {
|
||||||
yield try parse_primary(state)
|
yield try parse_primary(state)
|
||||||
}
|
}
|
||||||
|
left_grouped := parenthesized_guard(state, expression_start)
|
||||||
|
|
||||||
while true {
|
while true {
|
||||||
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)
|
||||||
|
left_grouped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (token.kind == .range or token.kind == .range_inclusive) {
|
||||||
|
if (min_bp > BindingPowerScalar(0)) break
|
||||||
|
left = try validate_range_bound(state, left, left_grouped)
|
||||||
|
state.next_token += 1
|
||||||
|
right_start :: state.next_token
|
||||||
|
right := try parse_expr_bp(state, BindingPowerScalar(1))
|
||||||
|
right = try validate_range_bound(state, right, parenthesized_guard(state, right_start))
|
||||||
|
left = try ast.add_range(
|
||||||
|
&state.builder,
|
||||||
|
ast.Span{
|
||||||
|
start = state.tokens[expression_start].span.start,
|
||||||
|
end = state.tokens[state.next_token - 1].span.end,
|
||||||
|
},
|
||||||
|
left, right, token.kind == .range_inclusive,
|
||||||
|
)
|
||||||
|
left_grouped = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (infix_binding_power(token.kind)) |power| {
|
if (infix_binding_power(token.kind)) |power| {
|
||||||
@@ -470,6 +579,7 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
|
|||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
)
|
)
|
||||||
|
left_grouped = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -477,6 +587,21 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
|
|||||||
return left
|
return left
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
validate_range_bound proc(
|
||||||
|
state @mut State, bound ast.NodeId, grouped bool,
|
||||||
|
) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
if (grouped) return bound
|
||||||
|
match ast.builder_expr_kind(&state.builder, bound) {
|
||||||
|
.unary, .binary, .range: {
|
||||||
|
span :: ast.builder_span(&state.builder, bound)
|
||||||
|
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, span)
|
||||||
|
return try ast.add_invalid_expr(&state.builder, span, diagnostic)
|
||||||
|
}
|
||||||
|
else: return bound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@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) ast.NodeId ! ParseError {
|
||||||
_ = try consume(state, .open_paren)
|
_ = try consume(state, .open_paren)
|
||||||
@@ -566,7 +691,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
@hide:file
|
@hide:file
|
||||||
is_expression_boundary proc(kind lexer.TokenKind) bool {
|
is_expression_boundary proc(kind lexer.TokenKind) bool {
|
||||||
return match kind {
|
return match kind {
|
||||||
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .eof: true
|
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .for, .eof: true
|
||||||
else: false
|
else: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -586,6 +711,7 @@ is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
|
|||||||
kind == .comma
|
kind == .comma
|
||||||
or kind == .and
|
or kind == .and
|
||||||
or kind == .or
|
or kind == .or
|
||||||
|
or kind == .pipe
|
||||||
or is_expression_boundary(kind)
|
or is_expression_boundary(kind)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import "@std/mem"
|
import "@std/mem"
|
||||||
import "@std/testing"
|
import "@std/testing"
|
||||||
|
import "@std/arraylist"
|
||||||
|
|
||||||
import "@source/diagnostics"
|
import "@source/diagnostics"
|
||||||
import "@source/strpool"
|
import "@source/strpool"
|
||||||
@@ -1083,3 +1084,119 @@ preserves_control_bodies_after_malformed_guards test {
|
|||||||
}
|
}
|
||||||
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 3)))
|
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 3)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parses_range_endpoints_and_capture_shapes 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 ::
|
||||||
|
`stored :: (1 + 2)..=(limit() * 2)
|
||||||
|
`run proc() void {
|
||||||
|
` for (-2)..1 |item| { item }
|
||||||
|
` for (0..10) |_| continue
|
||||||
|
` for 0..10 |@item, _| {}
|
||||||
|
` for 0..10 |item, index| { index }
|
||||||
|
` for
|
||||||
|
` 0..10
|
||||||
|
` |item|
|
||||||
|
` { item }
|
||||||
|
`}
|
||||||
|
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))
|
||||||
|
stored :: ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer
|
||||||
|
interval :: ast.range_expr(&tree, stored)
|
||||||
|
try testing.expect(interval.inclusive)
|
||||||
|
try testing.expect_equal(ast.ExprKind.binary, ast.expr_kind(&tree, interval.start))
|
||||||
|
try testing.expect_equal(ast.ExprKind.binary, ast.expr_kind(&tree, interval.end))
|
||||||
|
body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body
|
||||||
|
try testing.expect_equal(5, ast.block_statement_count(&tree, body))
|
||||||
|
first :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||||
|
if (first.capture) |capture| {
|
||||||
|
try testing.expect_equal(ast.ExprKind.identifier, ast.expr_kind(&tree, capture))
|
||||||
|
try testing.expect_equal("item", ast.source_text(&tree, ast.node_span(&tree, capture)))
|
||||||
|
} else return .expectation_failed
|
||||||
|
second :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 1))
|
||||||
|
try testing.expect(second.capture == null)
|
||||||
|
try testing.expect_equal(ast.StmtKind.continue_stmt, ast.stmt_kind(
|
||||||
|
&tree, ast.block_statement(&tree, second.body, 0),
|
||||||
|
))
|
||||||
|
third :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 2))
|
||||||
|
try testing.expect(third.pointer_capture and third.index_capture and third.index == null)
|
||||||
|
fourth :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 3))
|
||||||
|
try testing.expect(fourth.index_capture and fourth.index != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
rejects_bro_invalid_range_headers_without_losing_siblings test {
|
||||||
|
strpool.strings = strpool.init(mem.c_allocator)
|
||||||
|
defer strpool.deinit(&strpool.strings)
|
||||||
|
cases [13][]u8 :: [
|
||||||
|
"for -2..1 |i| {}",
|
||||||
|
"for 0..limit+1 |i| {}",
|
||||||
|
"for (0..limit+1) |i| {}",
|
||||||
|
"for 0..10 |i| continue",
|
||||||
|
"for (0)..10 |i| continue",
|
||||||
|
"for 0..10 {}",
|
||||||
|
"for 0..10 || {}",
|
||||||
|
"for 0..10 |i int| {}",
|
||||||
|
"for 0..10 |i,| {}",
|
||||||
|
"for 0..10 |i, j, k| {}",
|
||||||
|
"for 0..10 |i\n| {}",
|
||||||
|
"for ..10 |i| {}",
|
||||||
|
"for 0.. |i| {}",
|
||||||
|
]
|
||||||
|
for (cases) |header| {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
source arraylist.ArrayList(u8) := arraylist.init(mem.c_allocator)
|
||||||
|
defer arraylist.deinit(&source)
|
||||||
|
for ("run proc() void {\n") |byte| try arraylist.append(&source, byte)
|
||||||
|
for (header) |byte| try arraylist.append(&source, byte)
|
||||||
|
for ("\nkept :: 7\n}\nafter :: 9\n") |byte| try arraylist.append(&source, byte)
|
||||||
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source.items, &diagnostic_store)
|
||||||
|
tree := try parse(mem.c_allocator, scan_state.tokens.items, source.items, &diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
try testing.expect(diagnostics.count(&diagnostic_store) > 0)
|
||||||
|
try testing.expect_equal(2, ast.package_decl_count(&tree))
|
||||||
|
try testing.expect_equal("after", ast.source_text(
|
||||||
|
&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 1)).name,
|
||||||
|
))
|
||||||
|
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||||
|
last :: ast.block_statement(&tree, body, ast.block_statement_count(&tree, body) - 1)
|
||||||
|
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, last))
|
||||||
|
try testing.expect_equal("kept", ast.source_text(&tree, ast.statement_decl(&tree, last).name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
range_loop_preserves_nearest_else_and_canonical_body 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 ::
|
||||||
|
`run proc() void {
|
||||||
|
` if (true) for (0..2) |i| if (false) break else continue
|
||||||
|
` for values() |item| 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))
|
||||||
|
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||||
|
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||||
|
try testing.expect(outer.else_block == null)
|
||||||
|
loop :: ast.for_stmt(&tree, ast.block_statement(&tree, outer.then_block, 0))
|
||||||
|
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 0))
|
||||||
|
try testing.expect(nested.else_block != null)
|
||||||
|
call_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 1))
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|||||||
+102
-1
@@ -5,7 +5,17 @@ import "@source/ast"
|
|||||||
import "@source/diagnostics"
|
import "@source/diagnostics"
|
||||||
sema :: import "@source/sema"
|
sema :: import "@source/sema"
|
||||||
|
|
||||||
TypeKind :: enum { none, invalid, int, float, bool, string, void, function }
|
TypeKind :: enum {
|
||||||
|
none,
|
||||||
|
invalid,
|
||||||
|
int,
|
||||||
|
float,
|
||||||
|
bool,
|
||||||
|
string,
|
||||||
|
void,
|
||||||
|
function,
|
||||||
|
range,
|
||||||
|
}
|
||||||
|
|
||||||
Type :: struct {
|
Type :: struct {
|
||||||
kind TypeKind = .none
|
kind TypeKind = .none
|
||||||
@@ -40,6 +50,8 @@ Role :: enum {
|
|||||||
if_stmt,
|
if_stmt,
|
||||||
assignment_stmt,
|
assignment_stmt,
|
||||||
while_stmt,
|
while_stmt,
|
||||||
|
for_stmt,
|
||||||
|
loop_capture,
|
||||||
loop_control,
|
loop_control,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +67,7 @@ Cell :: struct {
|
|||||||
fixed bool = false
|
fixed bool = false
|
||||||
local bool = false
|
local bool = false
|
||||||
assignment_value bool = false
|
assignment_value bool = false
|
||||||
|
for_iterable bool = false
|
||||||
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))
|
||||||
}
|
}
|
||||||
@@ -269,6 +282,14 @@ check proc(
|
|||||||
try demand(&state, ast.call_arg(tree, id, j), cells[usize(parameter)].type, .type_mismatch)
|
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.
|
||||||
for (0..count) |i| {
|
for (0..count) |i| {
|
||||||
@@ -316,6 +337,14 @@ check proc(
|
|||||||
try fail(&state, condition, .condition_not_bool, ast.node_span(tree, condition))
|
try fail(&state, condition, .condition_not_bool, ast.node_span(tree, condition))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (0..count) |i| {
|
||||||
|
if (cells[i].role != .for_stmt) continue
|
||||||
|
iterable :: ast.for_stmt(tree, ast.NodeId(i)).iterable
|
||||||
|
actual :: types[usize(iterable)]
|
||||||
|
if (actual.kind != .invalid and actual.kind != .range) {
|
||||||
|
try fail(&state, iterable, .invalid_for_iterable, ast.node_span(tree, iterable))
|
||||||
|
}
|
||||||
|
}
|
||||||
propagate(&state, queue)
|
propagate(&state, queue)
|
||||||
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)
|
||||||
@@ -514,6 +543,19 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError |
|
|||||||
try collect_block(state, function.body, function.return_type, false)
|
try collect_block(state, function.body, function.return_type, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
collect_capture proc(state @mut State, id, body ast.NodeId) void ! mem.AllocError {
|
||||||
|
state.cells[usize(id)].role = .loop_capture
|
||||||
|
state.cells[usize(id)].fixed = true
|
||||||
|
state.cells[usize(id)].local = true
|
||||||
|
state.cells[usize(id)].type = Type{ kind = .int }
|
||||||
|
binding :: sema.binding(state.resolution, id)
|
||||||
|
if (binding.kind == .invalid) {
|
||||||
|
state.cells[usize(id)].type = invalid_type(binding.data.diagnostic)
|
||||||
|
}
|
||||||
|
try depend(state, id, body)
|
||||||
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop bool) void ! (mem.AllocError | diagnostics.CapacityError) {
|
collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop bool) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
tree :: state.tree
|
tree :: state.tree
|
||||||
@@ -577,6 +619,29 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
.for_stmt: {
|
||||||
|
loop :: ast.for_stmt(tree, statement)
|
||||||
|
state.cells[usize(statement)].role = .for_stmt
|
||||||
|
state.cells[usize(statement)].type = Type{ kind = .void }
|
||||||
|
state.cells[usize(loop.iterable)].for_iterable = true
|
||||||
|
try collect_expression(state, loop.iterable)
|
||||||
|
try depend(state, loop.iterable, statement)
|
||||||
|
if (loop.pointer_capture) {
|
||||||
|
state.cells[usize(statement)].type = invalid_type(try diagnostics.add(
|
||||||
|
state.diagnostics, .range_pointer_capture, loop.span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
if (loop.index_capture) {
|
||||||
|
diagnostic :: try diagnostics.add(state.diagnostics, .range_index_capture, loop.span)
|
||||||
|
if (state.cells[usize(statement)].type.kind != .invalid) {
|
||||||
|
state.cells[usize(statement)].type = invalid_type(diagnostic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loop.capture) |capture| try collect_capture(state, capture, loop.body)
|
||||||
|
if (loop.index) |index| try collect_capture(state, index, loop.body)
|
||||||
|
try collect_block(state, loop.body, return_annotation, true)
|
||||||
|
try depend(state, loop.body, id)
|
||||||
|
}
|
||||||
.break_stmt, .continue_stmt: {
|
.break_stmt, .continue_stmt: {
|
||||||
state.cells[usize(statement)].role = .loop_control
|
state.cells[usize(statement)].role = .loop_control
|
||||||
state.cells[usize(statement)].type = Type{ kind = .void }
|
state.cells[usize(statement)].type = Type{ kind = .void }
|
||||||
@@ -643,6 +708,19 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
|
|||||||
# the RHS may be skipped, so its errors do not erase the bool result.
|
# the RHS may be skipped, so its errors do not erase the bool result.
|
||||||
if (!logical(expression.operator)) try depend(state, expression.right, id)
|
if (!logical(expression.operator)) try depend(state, expression.right, id)
|
||||||
}
|
}
|
||||||
|
.range: {
|
||||||
|
expression :: ast.range_expr(tree, id)
|
||||||
|
state.cells[usize(id)].type = Type{ kind = .range }
|
||||||
|
if (!state.cells[usize(id)].for_iterable) {
|
||||||
|
state.cells[usize(id)].type = invalid_type(try diagnostics.add(
|
||||||
|
state.diagnostics, .range_value_not_supported, expression.span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
try collect_expression(state, expression.start)
|
||||||
|
try collect_expression(state, expression.end)
|
||||||
|
try depend(state, expression.start, id)
|
||||||
|
try depend(state, expression.end, id)
|
||||||
|
}
|
||||||
.call: {
|
.call: {
|
||||||
callee :: ast.call_expr(tree, id).callee
|
callee :: ast.call_expr(tree, id).callee
|
||||||
try collect_expression(state, callee)
|
try collect_expression(state, callee)
|
||||||
@@ -748,6 +826,18 @@ demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Cod
|
|||||||
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
|
||||||
|
range_bound_context proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
|
if (state.types[usize(id)].kind == .invalid) return
|
||||||
|
index :: root(state, usize(id))
|
||||||
|
if (state.cells[index].type.kind == .invalid) return
|
||||||
|
if (state.cells[index].demand == .float) {
|
||||||
|
try fail(state, id, .invalid_range_bound, ast.node_span(state.tree, id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try demand(state, id, Type{ kind = .int }, .invalid_range_bound)
|
||||||
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
fail proc(state @mut State, id ast.NodeId, code diagnostics.Code, span ast.Span) void ! (mem.AllocError | diagnostics.CapacityError) {
|
fail proc(state @mut State, id ast.NodeId, code diagnostics.Code, span ast.Span) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
if (state.types[usize(id)].kind == .invalid) return
|
if (state.types[usize(id)].kind == .invalid) return
|
||||||
@@ -861,6 +951,17 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
|
|||||||
try fail(state, id, .integer_division, expression.span)
|
try fail(state, id, .integer_division, expression.span)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.range: {
|
||||||
|
expression :: ast.range_expr(tree, id)
|
||||||
|
start_type :: state.types[usize(expression.start)]
|
||||||
|
end_type :: state.types[usize(expression.end)]
|
||||||
|
if (start_type.kind != .invalid and start_type.kind != .int) {
|
||||||
|
try fail(state, expression.start, .invalid_range_bound, ast.node_span(tree, expression.start))
|
||||||
|
}
|
||||||
|
if (end_type.kind != .invalid and end_type.kind != .int) {
|
||||||
|
try fail(state, expression.end, .invalid_range_bound, ast.node_span(tree, expression.end))
|
||||||
|
}
|
||||||
|
}
|
||||||
.call: try validate_call(state, id)
|
.call: try validate_call(state, id)
|
||||||
else: {}
|
else: {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1692,3 +1692,440 @@ loop_controls_do_not_guarantee_function_returns test {
|
|||||||
try testing.expect_equal(1, code_count(&diagnostic_store, .break_outside_loop))
|
try testing.expect_equal(1, code_count(&diagnostic_store, .break_outside_loop))
|
||||||
try testing.expect_equal(5, diagnostics.count(&diagnostic_store))
|
try testing.expect_equal(5, diagnostics.count(&diagnostic_store))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
range_bounds_receive_integer_context_without_joining_the_range_or_capture 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 ::
|
||||||
|
`limit :: seed
|
||||||
|
`seed := 3
|
||||||
|
`endpoint proc() int { return 4 }
|
||||||
|
`consume proc(value int) void {}
|
||||||
|
`main proc() void {
|
||||||
|
` for (-2)..(limit + 1) |item| {
|
||||||
|
` consume(item)
|
||||||
|
` copy :: item
|
||||||
|
` while false { consume(copy) }
|
||||||
|
` }
|
||||||
|
` for (0..=endpoint()) |_| consume(1)
|
||||||
|
` for 0..2 |item| { consume(item) }
|
||||||
|
`}
|
||||||
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||||
|
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||||
|
defer sema.deinit(&resolution)
|
||||||
|
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||||
|
defer deinit(&result)
|
||||||
|
|
||||||
|
try expect_kind(&result, ast.package_decl(&tree, 0), .int)
|
||||||
|
try expect_kind(&result, ast.package_decl(&tree, 1), .int)
|
||||||
|
main_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
|
||||||
|
for (usize(0)..usize(3)) |index| {
|
||||||
|
statement :: ast.block_statement(&tree, main_body, index)
|
||||||
|
loop :: ast.for_stmt(&tree, statement)
|
||||||
|
range_view :: ast.range_expr(&tree, loop.iterable)
|
||||||
|
try expect_kind(&result, statement, .void)
|
||||||
|
try expect_kind(&result, loop.iterable, .range)
|
||||||
|
try expect_kind(&result, range_view.start, .int)
|
||||||
|
try expect_kind(&result, range_view.end, .int)
|
||||||
|
if (loop.capture) |capture| {
|
||||||
|
try expect_kind(&result, capture, .int)
|
||||||
|
try testing.expect_equal(capture, sema.binding(&resolution, capture).data.declaration)
|
||||||
|
call :: ast.statement_expr(&tree, ast.block_statement(&tree, loop.body, 0))
|
||||||
|
item :: ast.call_arg(&tree, call, 0)
|
||||||
|
try expect_kind(&result, item, .int)
|
||||||
|
try testing.expect_equal(capture, sema.binding(&resolution, item).data.declaration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first :: ast.for_stmt(&tree, ast.block_statement(&tree, main_body, 0))
|
||||||
|
first_range :: ast.range_expr(&tree, first.iterable)
|
||||||
|
try expect_kind(&result, ast.unary_expr(&tree, first_range.start).operand, .int)
|
||||||
|
upper :: ast.binary_expr(&tree, first_range.end)
|
||||||
|
try expect_kind(&result, upper.left, .int)
|
||||||
|
try expect_kind(&result, upper.right, .int)
|
||||||
|
try expect_kind(&result, ast.block_statement(&tree, first.body, 1), .int)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|
||||||
|
range_checks_both_bounds_and_preserves_fixed_declarations_and_source_errors 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 ::
|
||||||
|
`real float :: 2
|
||||||
|
`flag :: true
|
||||||
|
`alias :: flag
|
||||||
|
`nothing proc() void {}
|
||||||
|
`main proc() void {
|
||||||
|
` for 1.5..2.5 |_| {}
|
||||||
|
` for alias.."text" |_| {}
|
||||||
|
` for nothing..nothing() |_| {}
|
||||||
|
` for real..3 |_| {}
|
||||||
|
` for absent_start..absent_end |_| {}
|
||||||
|
` for 0..1 |_| { sibling :: 7 }
|
||||||
|
` for flexible..floating_alias |_| {}
|
||||||
|
`}
|
||||||
|
`floating float :: floating_alias
|
||||||
|
`floating_alias :: flexible
|
||||||
|
`flexible := 3
|
||||||
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||||
|
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||||
|
defer sema.deinit(&resolution)
|
||||||
|
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||||
|
defer deinit(&result)
|
||||||
|
|
||||||
|
try expect_kind(&result, ast.package_decl(&tree, 0), .float)
|
||||||
|
try expect_kind(&result, ast.package_decl(&tree, 1), .bool)
|
||||||
|
try expect_kind(&result, ast.package_decl(&tree, 2), .bool)
|
||||||
|
try expect_kind(&result, ast.package_decl(&tree, 3), .function)
|
||||||
|
main_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
|
||||||
|
for (usize(0)..usize(3)) |index| {
|
||||||
|
statement :: ast.block_statement(&tree, main_body, index)
|
||||||
|
loop :: ast.for_stmt(&tree, statement)
|
||||||
|
range_view :: ast.range_expr(&tree, loop.iterable)
|
||||||
|
first_error :: try expect_error(&result, &diagnostic_store, range_view.start, .invalid_range_bound, ast.node_span(&tree, range_view.start))
|
||||||
|
second_error :: try expect_error(&result, &diagnostic_store, range_view.end, .invalid_range_bound, ast.node_span(&tree, range_view.end))
|
||||||
|
try testing.expect(first_error != second_error)
|
||||||
|
try testing.expect_equal(type_of(&result, loop.iterable).diagnostic, type_of(&result, statement).diagnostic)
|
||||||
|
}
|
||||||
|
mixed :: ast.for_stmt(&tree, ast.block_statement(&tree, main_body, 3))
|
||||||
|
mixed_range :: ast.range_expr(&tree, mixed.iterable)
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, mixed_range.start, .invalid_range_bound, ast.node_span(&tree, mixed_range.start))
|
||||||
|
try expect_kind(&result, mixed_range.end, .int)
|
||||||
|
unknown :: ast.for_stmt(&tree, ast.block_statement(&tree, main_body, 4))
|
||||||
|
unknown_range :: ast.range_expr(&tree, unknown.iterable)
|
||||||
|
unknown_bounds [2]ast.NodeId :: [unknown_range.start, unknown_range.end]
|
||||||
|
for unknown_bounds |bound| {
|
||||||
|
original :: sema.binding(&resolution, bound).data.diagnostic
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, bound, .unknown_name, ast.node_span(&tree, bound))
|
||||||
|
try testing.expect_equal(original, type_of(&result, bound).diagnostic)
|
||||||
|
}
|
||||||
|
sibling :: ast.for_stmt(&tree, ast.block_statement(&tree, main_body, 5))
|
||||||
|
try expect_kind(&result, sibling.iterable, .range)
|
||||||
|
try expect_kind(&result, ast.block_statement(&tree, sibling.body, 0), .int)
|
||||||
|
floating_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, main_body, 6))
|
||||||
|
floating_range :: ast.range_expr(&tree, floating_loop.iterable)
|
||||||
|
floating_start_error :: try expect_error(&result, &diagnostic_store, floating_range.start, .invalid_range_bound, ast.node_span(&tree, floating_range.start))
|
||||||
|
floating_end_error :: try expect_error(&result, &diagnostic_store, floating_range.end, .invalid_range_bound, ast.node_span(&tree, floating_range.end))
|
||||||
|
try testing.expect(floating_start_error != floating_end_error)
|
||||||
|
for (usize(5)..usize(8)) |index| try expect_kind(&result, ast.package_decl(&tree, index), .float)
|
||||||
|
try testing.expect_equal(9, code_count(&diagnostic_store, .invalid_range_bound))
|
||||||
|
try testing.expect_equal(2, code_count(&diagnostic_store, .unknown_name))
|
||||||
|
try testing.expect_equal(0, code_count(&diagnostic_store, .invalid_for_iterable))
|
||||||
|
try testing.expect_equal(0, code_count(&diagnostic_store, .cannot_infer_type))
|
||||||
|
}
|
||||||
|
|
||||||
|
range_values_nonrange_iterables_and_capture_modes_have_explicit_errors 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 ::
|
||||||
|
`stored :: 0..3
|
||||||
|
`produced proc() int { return 0..3 }
|
||||||
|
`consume proc(value int) void {}
|
||||||
|
`scalar proc() int { return 2 }
|
||||||
|
`main proc() void {
|
||||||
|
` 0..3
|
||||||
|
` consume(0..3)
|
||||||
|
` local :: 0..3
|
||||||
|
` for 3 |_| { absent_scalar }
|
||||||
|
` for true |_| {}
|
||||||
|
` for "text" |_| {}
|
||||||
|
` for scalar() |_| {}
|
||||||
|
` for stored |_| {}
|
||||||
|
` for 0..3 |@item| { item }
|
||||||
|
` for 0..3 |item, index| { item + index }
|
||||||
|
` for 0..3 |_, _| {}
|
||||||
|
` for 0..3 |@item, _| {}
|
||||||
|
`}
|
||||||
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||||
|
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||||
|
defer sema.deinit(&resolution)
|
||||||
|
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||||
|
defer deinit(&result)
|
||||||
|
|
||||||
|
stored :: ast.package_decl(&tree, 0)
|
||||||
|
stored_value :: ast.statement_decl(&tree, stored).initializer
|
||||||
|
stored_error :: try expect_error(&result, &diagnostic_store, stored_value, .range_value_not_supported, ast.node_span(&tree, stored_value))
|
||||||
|
try testing.expect_equal(stored_error, type_of(&result, stored).diagnostic)
|
||||||
|
produced_body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body
|
||||||
|
returned :: ast.return_stmt(&tree, ast.block_statement(&tree, produced_body, 0)).value?
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, returned, .range_value_not_supported, ast.node_span(&tree, returned))
|
||||||
|
main_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
|
||||||
|
standalone :: ast.statement_expr(&tree, ast.block_statement(&tree, main_body, 0))
|
||||||
|
call :: ast.statement_expr(&tree, ast.block_statement(&tree, main_body, 1))
|
||||||
|
local_value :: ast.statement_decl(&tree, ast.block_statement(&tree, main_body, 2)).initializer
|
||||||
|
values [3]ast.NodeId :: [standalone, ast.call_arg(&tree, call, 0), local_value]
|
||||||
|
for values |value| {
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, value, .range_value_not_supported, ast.node_span(&tree, value))
|
||||||
|
}
|
||||||
|
for (usize(3)..usize(7)) |index| {
|
||||||
|
statement :: ast.block_statement(&tree, main_body, index)
|
||||||
|
loop :: ast.for_stmt(&tree, statement)
|
||||||
|
original :: try expect_error(&result, &diagnostic_store, loop.iterable, .invalid_for_iterable, ast.node_span(&tree, loop.iterable))
|
||||||
|
try testing.expect_equal(original, type_of(&result, statement).diagnostic)
|
||||||
|
}
|
||||||
|
stored_loop :: ast.block_statement(&tree, main_body, 7)
|
||||||
|
try testing.expect_equal(stored_error, type_of(&result, stored_loop).diagnostic)
|
||||||
|
modes [4]diagnostics.Code :: [.range_pointer_capture, .range_index_capture, .range_index_capture, .range_pointer_capture]
|
||||||
|
for modes |code, index| {
|
||||||
|
statement :: ast.block_statement(&tree, main_body, index + 8)
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, statement, code, ast.node_span(&tree, statement))
|
||||||
|
}
|
||||||
|
indexed :: ast.for_stmt(&tree, ast.block_statement(&tree, main_body, 9))
|
||||||
|
try expect_kind(&result, indexed.capture?, .int)
|
||||||
|
try expect_kind(&result, indexed.index?, .int)
|
||||||
|
try expect_kind(&result, ast.statement_expr(&tree, ast.block_statement(&tree, indexed.body, 0)), .int)
|
||||||
|
try testing.expect_equal(5, code_count(&diagnostic_store, .range_value_not_supported))
|
||||||
|
try testing.expect_equal(4, code_count(&diagnostic_store, .invalid_for_iterable))
|
||||||
|
try testing.expect_equal(2, code_count(&diagnostic_store, .range_pointer_capture))
|
||||||
|
try testing.expect_equal(3, code_count(&diagnostic_store, .range_index_capture))
|
||||||
|
try testing.expect_equal(1, code_count(&diagnostic_store, .unknown_name))
|
||||||
|
try testing.expect_equal(0, code_count(&diagnostic_store, .return_type_mismatch))
|
||||||
|
try testing.expect_equal(0, code_count(&diagnostic_store, .type_mismatch))
|
||||||
|
}
|
||||||
|
|
||||||
|
range_captures_are_fixed_readonly_and_assignment_values_do_not_retarget_bounds 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 ::
|
||||||
|
`consume proc(value float) void {}
|
||||||
|
`returned proc() float {
|
||||||
|
` for 0..2 |item| { return item }
|
||||||
|
` return 1
|
||||||
|
`}
|
||||||
|
`main proc() void {
|
||||||
|
` limit := 3
|
||||||
|
` real := 1.5
|
||||||
|
` for 0..limit |item| {
|
||||||
|
` item = 2
|
||||||
|
` if true { item = 1.5 }
|
||||||
|
` while false { item = -"bad" }
|
||||||
|
` real = item
|
||||||
|
` real = limit
|
||||||
|
` consume(item)
|
||||||
|
` copy :: item
|
||||||
|
` consume(copy)
|
||||||
|
` }
|
||||||
|
`}
|
||||||
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||||
|
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||||
|
defer sema.deinit(&resolution)
|
||||||
|
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||||
|
defer deinit(&result)
|
||||||
|
|
||||||
|
returned_body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body
|
||||||
|
returned_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, returned_body, 0))
|
||||||
|
returned_value :: ast.return_stmt(&tree, ast.block_statement(&tree, returned_loop.body, 0)).value?
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, returned_value, .return_type_mismatch, ast.node_span(&tree, returned_value))
|
||||||
|
try expect_kind(&result, returned_loop.capture?, .int)
|
||||||
|
main_body :: ast.function_decl(&tree, ast.package_decl(&tree, 2)).body
|
||||||
|
limit :: ast.block_statement(&tree, main_body, 0)
|
||||||
|
real :: ast.block_statement(&tree, main_body, 1)
|
||||||
|
loop_statement :: ast.block_statement(&tree, main_body, 2)
|
||||||
|
loop :: ast.for_stmt(&tree, loop_statement)
|
||||||
|
try expect_kind(&result, limit, .int)
|
||||||
|
try expect_kind(&result, real, .float)
|
||||||
|
try expect_kind(&result, loop_statement, .void)
|
||||||
|
try expect_kind(&result, loop.iterable, .range)
|
||||||
|
try expect_kind(&result, ast.range_expr(&tree, loop.iterable).end, .int)
|
||||||
|
try expect_kind(&result, loop.capture?, .int)
|
||||||
|
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 1))
|
||||||
|
inner :: ast.while_stmt(&tree, ast.block_statement(&tree, loop.body, 2))
|
||||||
|
readonly [3]ast.NodeId :: [
|
||||||
|
ast.block_statement(&tree, loop.body, 0),
|
||||||
|
ast.block_statement(&tree, conditional.then_block, 0),
|
||||||
|
ast.block_statement(&tree, inner.body, 0),
|
||||||
|
]
|
||||||
|
for readonly |statement| {
|
||||||
|
assignment :: ast.assignment_stmt(&tree, statement)
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, statement, .invalid_assignment_target, ast.node_span(&tree, assignment.target))
|
||||||
|
try expect_kind(&result, assignment.target, .int)
|
||||||
|
}
|
||||||
|
dead_value :: ast.assignment_stmt(&tree, readonly[2]).value
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, dead_value, .invalid_operand, ast.node_span(&tree, dead_value))
|
||||||
|
for (usize(3)..usize(5)) |index| {
|
||||||
|
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, loop.body, index))
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, assignment.value, .type_mismatch, ast.node_span(&tree, assignment.value))
|
||||||
|
try expect_kind(&result, assignment.target, .float)
|
||||||
|
}
|
||||||
|
for (usize(5)..usize(8)) |index| {
|
||||||
|
if (index == 6) continue
|
||||||
|
call :: ast.statement_expr(&tree, ast.block_statement(&tree, loop.body, index))
|
||||||
|
argument :: ast.call_arg(&tree, call, 0)
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, argument, .type_mismatch, ast.node_span(&tree, argument))
|
||||||
|
}
|
||||||
|
try expect_kind(&result, ast.block_statement(&tree, loop.body, 6), .int)
|
||||||
|
try testing.expect_equal(3, code_count(&diagnostic_store, .invalid_assignment_target))
|
||||||
|
try testing.expect_equal(4, code_count(&diagnostic_store, .type_mismatch))
|
||||||
|
try testing.expect_equal(1, code_count(&diagnostic_store, .return_type_mismatch))
|
||||||
|
try testing.expect_equal(1, code_count(&diagnostic_store, .invalid_operand))
|
||||||
|
try testing.expect_equal(0, code_count(&diagnostic_store, .invalid_range_bound))
|
||||||
|
try testing.expect_equal(0, code_count(&diagnostic_store, .missing_return))
|
||||||
|
}
|
||||||
|
|
||||||
|
range_loop_return_flow_and_controls_are_lexical_and_body_errors_are_isolated 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 ::
|
||||||
|
`missing proc() int { for 0..=0 |_| { return 1 } }
|
||||||
|
`bad_body proc() int { for 2..1 |_| { -"bad" } }
|
||||||
|
`breaking proc() int { for 0..2 |_| { break } }
|
||||||
|
`continuing proc() int { for 2..1 |_| { continue } }
|
||||||
|
`closed proc() int {
|
||||||
|
` for 2..1 |_| { -"bad" }
|
||||||
|
` return 7
|
||||||
|
`}
|
||||||
|
`scopes proc(flag bool) void {
|
||||||
|
` for 0..2 |item| {
|
||||||
|
` if (flag) break else continue
|
||||||
|
` while flag {
|
||||||
|
` if (flag) continue else break
|
||||||
|
` for 0..item |_| { continue }
|
||||||
|
` }
|
||||||
|
` absent
|
||||||
|
` }
|
||||||
|
` while flag { for 0..2 |_| { break } }
|
||||||
|
` break
|
||||||
|
` continue
|
||||||
|
` item
|
||||||
|
`}
|
||||||
|
`callee proc() void {
|
||||||
|
` break
|
||||||
|
` continue
|
||||||
|
`}
|
||||||
|
`caller proc() void {
|
||||||
|
` for 0..2 |_| {
|
||||||
|
` callee()
|
||||||
|
` break
|
||||||
|
` }
|
||||||
|
`}
|
||||||
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||||
|
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||||
|
defer sema.deinit(&resolution)
|
||||||
|
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||||
|
defer deinit(&result)
|
||||||
|
|
||||||
|
for (usize(0)..usize(4)) |index| {
|
||||||
|
function :: ast.package_decl(&tree, index)
|
||||||
|
body :: ast.function_decl(&tree, function).body
|
||||||
|
try expect_kind(&result, function, .function)
|
||||||
|
_ = try expect_error(&result, &diagnostic_store, body, .missing_return, ast.node_span(&tree, body))
|
||||||
|
try expect_kind(&result, ast.block_statement(&tree, body, 0), .void)
|
||||||
|
}
|
||||||
|
closed_body :: ast.function_decl(&tree, ast.package_decl(&tree, 4)).body
|
||||||
|
closed_statement :: ast.block_statement(&tree, closed_body, 0)
|
||||||
|
closed_loop :: ast.for_stmt(&tree, closed_statement)
|
||||||
|
dead_value :: ast.statement_expr(&tree, ast.block_statement(&tree, closed_loop.body, 0))
|
||||||
|
original :: try expect_error(&result, &diagnostic_store, dead_value, .invalid_operand, ast.node_span(&tree, dead_value))
|
||||||
|
try testing.expect_equal(original, type_of(&result, closed_body).diagnostic)
|
||||||
|
try expect_kind(&result, closed_statement, .void)
|
||||||
|
try expect_kind(&result, ast.block_statement(&tree, closed_body, 1), .void)
|
||||||
|
scopes_body :: ast.function_decl(&tree, ast.package_decl(&tree, 5)).body
|
||||||
|
outer_statement :: ast.block_statement(&tree, scopes_body, 0)
|
||||||
|
outer :: ast.for_stmt(&tree, outer_statement)
|
||||||
|
conditional :: ast.if_stmt(&tree, ast.block_statement(&tree, outer.body, 0))
|
||||||
|
inner_statement :: ast.block_statement(&tree, outer.body, 1)
|
||||||
|
inner :: ast.while_stmt(&tree, inner_statement)
|
||||||
|
inner_if :: ast.if_stmt(&tree, ast.block_statement(&tree, inner.body, 0))
|
||||||
|
nested_statement :: ast.block_statement(&tree, inner.body, 1)
|
||||||
|
nested :: ast.for_stmt(&tree, nested_statement)
|
||||||
|
controls [5]ast.NodeId :: [
|
||||||
|
ast.block_statement(&tree, conditional.then_block, 0),
|
||||||
|
ast.block_statement(&tree, conditional.else_block?, 0),
|
||||||
|
ast.block_statement(&tree, inner_if.then_block, 0),
|
||||||
|
ast.block_statement(&tree, inner_if.else_block?, 0),
|
||||||
|
ast.block_statement(&tree, nested.body, 0),
|
||||||
|
]
|
||||||
|
for (controls) |statement| try expect_kind(&result, statement, .void)
|
||||||
|
try expect_kind(&result, outer_statement, .void)
|
||||||
|
try expect_kind(&result, inner_statement, .void)
|
||||||
|
try expect_kind(&result, nested_statement, .void)
|
||||||
|
absent :: ast.statement_expr(&tree, ast.block_statement(&tree, outer.body, 2))
|
||||||
|
try testing.expect_equal(sema.binding(&resolution, absent).data.diagnostic, type_of(&result, absent).diagnostic)
|
||||||
|
escaped :: ast.statement_expr(&tree, ast.block_statement(&tree, scopes_body, 4))
|
||||||
|
try testing.expect_equal(sema.binding(&resolution, escaped).data.diagnostic, type_of(&result, escaped).diagnostic)
|
||||||
|
try testing.expect_equal(4, code_count(&diagnostic_store, .missing_return))
|
||||||
|
try testing.expect_equal(2, code_count(&diagnostic_store, .invalid_operand))
|
||||||
|
try testing.expect_equal(2, code_count(&diagnostic_store, .unknown_name))
|
||||||
|
try testing.expect_equal(2, code_count(&diagnostic_store, .break_outside_loop))
|
||||||
|
try testing.expect_equal(2, code_count(&diagnostic_store, .continue_outside_loop))
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid_range_capture_bindings_keep_resolver_diagnostic_ids_without_erasing_headers 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 {
|
||||||
|
` existing :: 7
|
||||||
|
` for 0..1 |existing| { sibling :: 1 }
|
||||||
|
` for 0..1 |int| { sibling :: 2 }
|
||||||
|
` for 0..1 |item| { item :: 3 }
|
||||||
|
` for 0..1 |item| { copy :: item }
|
||||||
|
`}
|
||||||
|
scan_state := lexer.init(mem.c_allocator)
|
||||||
|
defer lexer.deinit(&scan_state)
|
||||||
|
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||||
|
tree := try parser.parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||||
|
defer ast.deinit(&tree)
|
||||||
|
resolution := try sema.resolve(mem.c_allocator, &tree, &diagnostic_store)
|
||||||
|
defer sema.deinit(&resolution)
|
||||||
|
previous_count :: diagnostics.count(&diagnostic_store)
|
||||||
|
result := try check(mem.c_allocator, &tree, &resolution, &diagnostic_store)
|
||||||
|
defer deinit(&result)
|
||||||
|
|
||||||
|
function :: ast.package_decl(&tree, 0)
|
||||||
|
body :: ast.function_decl(&tree, function).body
|
||||||
|
try expect_kind(&result, function, .function)
|
||||||
|
try expect_kind(&result, ast.block_statement(&tree, body, 0), .int)
|
||||||
|
for (usize(1)..usize(3)) |index| {
|
||||||
|
statement :: ast.block_statement(&tree, body, index)
|
||||||
|
loop :: ast.for_stmt(&tree, statement)
|
||||||
|
capture :: loop.capture?
|
||||||
|
binding :: sema.binding(&resolution, capture)
|
||||||
|
try testing.expect_equal(sema.BindingKind.invalid, binding.kind)
|
||||||
|
try testing.expect_equal(TypeKind.invalid, type_of(&result, capture).kind)
|
||||||
|
try testing.expect_equal(binding.data.diagnostic, type_of(&result, capture).diagnostic)
|
||||||
|
try testing.expect_equal(binding.data.diagnostic, type_of(&result, loop.body).diagnostic)
|
||||||
|
try expect_kind(&result, statement, .void)
|
||||||
|
try expect_kind(&result, loop.iterable, .range)
|
||||||
|
try expect_kind(&result, ast.block_statement(&tree, loop.body, 0), .int)
|
||||||
|
}
|
||||||
|
duplicate_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 3))
|
||||||
|
duplicate :: ast.block_statement(&tree, duplicate_loop.body, 0)
|
||||||
|
try expect_kind(&result, duplicate_loop.capture?, .int)
|
||||||
|
try testing.expect_equal(sema.binding(&resolution, duplicate).data.diagnostic, type_of(&result, duplicate).diagnostic)
|
||||||
|
sibling_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 4))
|
||||||
|
try expect_kind(&result, sibling_loop.capture?, .int)
|
||||||
|
try expect_kind(&result, ast.block_statement(&tree, sibling_loop.body, 0), .int)
|
||||||
|
try testing.expect_equal(previous_count, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|||||||
@@ -220,6 +220,19 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me
|
|||||||
try resolve_expr(state, scope, loop.condition)
|
try resolve_expr(state, scope, loop.condition)
|
||||||
try resolve_arm(state, scope, loop.body)
|
try resolve_arm(state, scope, loop.body)
|
||||||
}
|
}
|
||||||
|
.for_stmt: {
|
||||||
|
loop :: ast.for_stmt(tree, statement)
|
||||||
|
try resolve_expr(state, scope, loop.iterable)
|
||||||
|
body_scope := Scope{ names = hashmap.init(state.allocator), parent = scope }
|
||||||
|
defer hashmap.deinit(&body_scope.names)
|
||||||
|
if (loop.capture) |capture| {
|
||||||
|
try predeclare(state, &body_scope, capture, ast.node_span(tree, capture))
|
||||||
|
}
|
||||||
|
if (loop.index) |index| {
|
||||||
|
try predeclare(state, &body_scope, index, ast.node_span(tree, index))
|
||||||
|
}
|
||||||
|
try resolve_block(state, &body_scope, loop.body)
|
||||||
|
}
|
||||||
.invalid: resolve_invalid(state, statement)
|
.invalid: resolve_invalid(state, statement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,6 +288,11 @@ resolve_expr proc(
|
|||||||
try resolve_expr(state, scope, expression.left)
|
try resolve_expr(state, scope, expression.left)
|
||||||
try resolve_expr(state, scope, expression.right)
|
try resolve_expr(state, scope, expression.right)
|
||||||
}
|
}
|
||||||
|
.range: {
|
||||||
|
expression :: ast.range_expr(tree, id)
|
||||||
|
try resolve_expr(state, scope, expression.start)
|
||||||
|
try resolve_expr(state, scope, expression.end)
|
||||||
|
}
|
||||||
.call: {
|
.call: {
|
||||||
try resolve_expr(state, scope, ast.call_expr(tree, id).callee)
|
try resolve_expr(state, scope, ast.call_expr(tree, id).callee)
|
||||||
for (0..ast.call_arg_count(tree, id)) |i| {
|
for (0..ast.call_arg_count(tree, id)) |i| {
|
||||||
|
|||||||
@@ -485,3 +485,121 @@ resolves_all_code_and_preserves_scopes_after_loop_controls test {
|
|||||||
forbidden :: ast.block_statement(&tree, conditional.then_block, 2)
|
forbidden :: ast.block_statement(&tree, conditional.then_block, 2)
|
||||||
try testing.expect_equal(BindingKind.invalid, binding(&resolution, forbidden).kind)
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, forbidden).kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
range_bounds_use_outer_scope_and_captures_are_body_declarations 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 ::
|
||||||
|
`run proc() void {
|
||||||
|
` for lower..upper |item| {
|
||||||
|
` before :: later
|
||||||
|
` later :: item
|
||||||
|
` continue
|
||||||
|
` item
|
||||||
|
` }
|
||||||
|
` for lower..upper |item| { item }
|
||||||
|
` lower :: 0
|
||||||
|
` upper :: 2
|
||||||
|
` item
|
||||||
|
`}
|
||||||
|
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)
|
||||||
|
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||||
|
loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||||
|
sibling :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 1))
|
||||||
|
lower :: ast.block_statement(&tree, body, 2)
|
||||||
|
upper :: ast.block_statement(&tree, body, 3)
|
||||||
|
bounds :: ast.range_expr(&tree, loop.iterable)
|
||||||
|
try testing.expect_equal(lower, binding(&resolution, bounds.start).data.declaration)
|
||||||
|
try testing.expect_equal(upper, binding(&resolution, bounds.end).data.declaration)
|
||||||
|
if (loop.capture) |capture| {
|
||||||
|
try testing.expect_equal(capture, binding(&resolution, capture).data.declaration)
|
||||||
|
later :: ast.block_statement(&tree, loop.body, 1)
|
||||||
|
before_ref :: ast.statement_decl(&tree, ast.block_statement(&tree, loop.body, 0)).initializer
|
||||||
|
try testing.expect_equal(later, binding(&resolution, before_ref).data.declaration)
|
||||||
|
capture_ref :: ast.statement_decl(&tree, later).initializer
|
||||||
|
try testing.expect_equal(capture, binding(&resolution, capture_ref).data.declaration)
|
||||||
|
dead_ref :: ast.statement_expr(&tree, ast.block_statement(&tree, loop.body, 3))
|
||||||
|
try testing.expect_equal(capture, binding(&resolution, dead_ref).data.declaration)
|
||||||
|
} else return .expectation_failed
|
||||||
|
if (sibling.capture) |capture| {
|
||||||
|
try testing.expect_equal(BindingKind.declaration, binding(&resolution, capture).kind)
|
||||||
|
sibling_ref :: ast.statement_expr(&tree, ast.block_statement(&tree, sibling.body, 0))
|
||||||
|
try testing.expect_equal(capture, binding(&resolution, sibling_ref).data.declaration)
|
||||||
|
} else return .expectation_failed
|
||||||
|
escaped :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 4))
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, escaped).kind)
|
||||||
|
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|
||||||
|
range_captures_preserve_duplicate_builtin_and_discard_errors 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 ::
|
||||||
|
`run proc() void {
|
||||||
|
` outer :: 0
|
||||||
|
` for outer..2 |outer| { outer }
|
||||||
|
` for 0..2 |int| { int }
|
||||||
|
` for 0..2 |item, item| { item }
|
||||||
|
` for 0..2 |local| {
|
||||||
|
` local :: 1
|
||||||
|
` local
|
||||||
|
` }
|
||||||
|
` for 0..2 |_| {
|
||||||
|
` break
|
||||||
|
` _
|
||||||
|
` missing
|
||||||
|
` }
|
||||||
|
` for missing_start..missing_end |i| {}
|
||||||
|
`}
|
||||||
|
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)
|
||||||
|
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||||
|
for (usize(1)..usize(3)) |index| {
|
||||||
|
loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, index))
|
||||||
|
if (loop.capture) |capture| {
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, capture).kind)
|
||||||
|
reference :: ast.statement_expr(&tree, ast.block_statement(&tree, loop.body, 0))
|
||||||
|
try testing.expect_equal(
|
||||||
|
binding(&resolution, capture).data.diagnostic,
|
||||||
|
binding(&resolution, reference).data.diagnostic,
|
||||||
|
)
|
||||||
|
} else return .expectation_failed
|
||||||
|
}
|
||||||
|
indexed :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 3))
|
||||||
|
if (indexed.index) |index| {
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, index).kind)
|
||||||
|
} else return .expectation_failed
|
||||||
|
if (indexed.capture) |capture| {
|
||||||
|
reference :: ast.statement_expr(&tree, ast.block_statement(&tree, indexed.body, 0))
|
||||||
|
try testing.expect_equal(capture, binding(&resolution, reference).data.declaration)
|
||||||
|
} else return .expectation_failed
|
||||||
|
local_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 4))
|
||||||
|
duplicate :: ast.block_statement(&tree, local_loop.body, 0)
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, duplicate).kind)
|
||||||
|
discard :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 5))
|
||||||
|
try testing.expect(discard.capture == null)
|
||||||
|
for (usize(1)..usize(3)) |index| {
|
||||||
|
reference :: ast.statement_expr(&tree, ast.block_statement(&tree, discard.body, index))
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, reference).kind)
|
||||||
|
}
|
||||||
|
missing_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 6))
|
||||||
|
bounds :: ast.range_expr(&tree, missing_loop.iterable)
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, bounds.start).kind)
|
||||||
|
try testing.expect_equal(BindingKind.invalid, binding(&resolution, bounds.end).kind)
|
||||||
|
try testing.expect_equal(8, diagnostics.count(&diagnostic_store))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user