feat: execute Bro-compatible integer range loops

This commit is contained in:
2026-10-10 01:47:30 +02:00
parent d51ca57c9e
commit cf39481bfc
16 changed files with 1416 additions and 16 deletions
+85 -4
View File
@@ -3,13 +3,13 @@
#! * binding ... -> :: | :=
#! * literal .. -> int | float | string | 'true' | 'false'
#! * 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
#! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks)
#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm
#! braceless guards require parentheses unless the condition is a call; else binds to the nearest if.
#! precedence (tightest first): calls, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or.
#! * stmt ..... -> decl | expr | identifier '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm
#! braceless guards/iterables require parentheses unless they are calls; else binds to the nearest if.
#! precedence (tightest first): calls, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses.
#! binary operators associate left; unary operators associate right.
import "@std/debug"
@@ -39,6 +39,7 @@ ExprKind :: enum {
unary
binary
call
range
invalid
}
@@ -55,6 +56,7 @@ StmtKind :: enum {
continue_stmt,
if_stmt,
while_stmt,
for_stmt,
invalid
}
@@ -83,6 +85,7 @@ NodeKind :: enum {
expr_unary,
expr_binary,
expr_call,
expr_range,
stmt_expr,
stmt_decl,
stmt_return,
@@ -91,6 +94,7 @@ NodeKind :: enum {
stmt_if,
stmt_assignment,
stmt_while,
stmt_for,
func_decl,
param_decl,
block,
@@ -201,6 +205,23 @@ WhileView :: struct {
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 {
span Span
diagnostic DiagnosticId
@@ -287,6 +308,18 @@ add_binary proc(
}, 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(
builder @mut Builder,
span Span,
@@ -368,6 +401,26 @@ add_while_stmt proc(
}, 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(
builder @mut Builder,
span Span,
@@ -508,6 +561,7 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
.stmt_if: .if_stmt
.stmt_assignment: .assignment_stmt
.stmt_while: .while_stmt
.stmt_for: .for_stmt
.invalid_stmt: .invalid
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 {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_decl)
@@ -640,6 +709,7 @@ expression_kind proc(kind NodeKind) ExprKind {
.expr_unary: .unary
.expr_binary: .binary
.expr_call: .call
.expr_range: .range
.invalid_expr: .invalid
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 {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_call)
+22
View File
@@ -94,6 +94,28 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept
try append_text(renderer, "\n")
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")
}
}
+10
View File
@@ -32,6 +32,11 @@ Code :: enum {
invalid_assignment_target,
break_outside_loop,
continue_outside_loop,
invalid_range_bound,
invalid_for_iterable,
range_value_not_supported,
range_pointer_capture,
range_index_capture,
missing_entry,
invalid_entry,
literal_out_of_range,
@@ -102,6 +107,11 @@ messages :: $enummap.init(Code, []u8, {
invalid_assignment_target = "assignment requires a mutable local binding",
break_outside_loop = "break 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",
invalid_entry = "main must be a function with no parameters",
literal_out_of_range = "numeric literal is outside its runtime type's range",
+18 -1
View File
@@ -134,6 +134,18 @@ scan proc(
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 cursor + 1 < program.len and program[cursor + 1] == ':' {
try add_token(state, Token{
@@ -233,6 +245,8 @@ scan proc(
'<': .less
'>': .greater
',': .comma
'|': .pipe
'@': .at
'(': .open_paren
')': .close_paren
'[': .open_bracket
@@ -304,7 +318,10 @@ scan_number proc(start usize, program []u8) ScanNumResult ! ScanError {
cursor := start
has_decimal := false
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
cursor += 1
}
+23 -1
View File
@@ -11,7 +11,7 @@ reports_invalid_input_and_keeps_scanning test {
state := init(mem.c_allocator)
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(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(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))
}
+3
View File
@@ -42,6 +42,9 @@ TokenKind :: enum {
plus, minus
star, slash
range, range_inclusive
pipe, at
# logical
bang
+84 -5
View File
@@ -18,7 +18,7 @@ Storage :: struct {
@hide:file
LoopTargets :: struct {
guard ir.BlockId
continue_target ir.BlockId
exit ir.BlockId
}
@@ -104,7 +104,7 @@ lower_type proc(type typecheck.Type) ir.Type {
.string: .string
.void: .void
.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)
}
.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: {}
}
}
@@ -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),
}
previous_loop :: state.loop
state.loop = LoopTargets{ guard = guard, exit = exit }
state.loop = LoopTargets{ continue_target = guard, exit = exit }
defer state.loop = previous_loop
state.block = body
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
}
@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
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
match ast.stmt_kind(tree, id) {
.if_stmt: try lower_if(state, id)
.while_stmt: try lower_while(state, id)
.for_stmt: try lower_for(state, id)
.break_stmt, .continue_stmt: {
if (trap_type(state, id)) return
if (state.loop) |targets| {
target := targets.exit
# continue restarts the whole guard, before any short-circuit branches.
if (ast.stmt_kind(tree, id) == .continue_stmt) target = targets.guard
# each loop supplies its own guard or guarded-update entry.
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{
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)
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: {
if (trap_type(state, id)) return ir.no_value
return try lower_number(state, id)
+235
View File
@@ -439,6 +439,15 @@ releases_temporaries_and_partially_owned_modules_on_allocation_failure test {
`main proc() string {
` computed float :: factor * 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))))) {
` pass :: identity(text)
` identity(pass)
@@ -1117,8 +1126,16 @@ lazy_branchful_global_initializers_and_calls_release_every_runtime_allocation te
`first :: true and later
`later :: false or !false
`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 {
` 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 {
` n = n - 1
` 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))
}
}
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
View File
@@ -185,6 +185,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
match current(state).kind {
.if: return try parse_if(state)
.while: return try parse_while(state, allow_else)
.for: return try parse_for(state, allow_else)
.return: return try parse_return(state, allow_else)
.break, .continue: return try parse_loop_control(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren: {
@@ -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
parse_guard proc(state @mut State) ast.NodeId ! ParseError {
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, .else) and !next_is(state, .return)
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)
) {
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 {
skip_operand_newlines(state)
token := current(state)
expression_start :: state.next_token
left := if (prefix_binding_power(token.kind)) |right_bp| {
state.next_token += 1
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 {
yield try parse_primary(state)
}
left_grouped := parenthesized_guard(state, expression_start)
while true {
skip_continuation_newlines(state)
token = current(state)
if (token.kind == .open_paren) {
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
}
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,
right,
)
left_grouped = false
continue
}
break
@@ -477,6 +587,21 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
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
parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError {
_ = try consume(state, .open_paren)
@@ -566,7 +691,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
@hide:file
is_expression_boundary proc(kind lexer.TokenKind) bool {
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
}
}
@@ -586,6 +711,7 @@ is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
kind == .comma
or kind == .and
or kind == .or
or kind == .pipe
or is_expression_boundary(kind)
)
}
+117
View File
@@ -1,5 +1,6 @@
import "@std/mem"
import "@std/testing"
import "@std/arraylist"
import "@source/diagnostics"
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)))
}
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
View File
@@ -5,7 +5,17 @@ import "@source/ast"
import "@source/diagnostics"
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 {
kind TypeKind = .none
@@ -40,6 +50,8 @@ Role :: enum {
if_stmt,
assignment_stmt,
while_stmt,
for_stmt,
loop_capture,
loop_control,
}
@@ -55,6 +67,7 @@ Cell :: struct {
fixed bool = false
local bool = false
assignment_value bool = false
for_iterable bool = false
first_edge usize = no_index
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)
}
}
# 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.
for (0..count) |i| {
@@ -316,6 +337,14 @@ check proc(
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)
for (0..ast.package_decl_count(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)
}
@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
collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop bool) void ! (mem.AllocError | diagnostics.CapacityError) {
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 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: {
state.cells[usize(statement)].role = .loop_control
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.
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: {
callee :: ast.call_expr(tree, id).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))
}
@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
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
@@ -861,6 +951,17 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
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)
else: {}
}
+437
View File
@@ -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(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))
}
+18
View File
@@ -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_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)
}
}
@@ -275,6 +288,11 @@ resolve_expr proc(
try resolve_expr(state, scope, expression.left)
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: {
try resolve_expr(state, scope, ast.call_expr(tree, id).callee)
for (0..ast.call_arg_count(tree, id)) |i| {
+118
View File
@@ -485,3 +485,121 @@ resolves_all_code_and_preserves_scopes_after_loop_controls test {
forbidden :: ast.block_statement(&tree, conditional.then_block, 2)
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))
}