From cf39481bfcf726e6b1b516aa64e5f9140dc48fff Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 10 Oct 2026 01:47:30 +0200 Subject: [PATCH] feat: execute Bro-compatible integer range loops --- README.md | 10 +- programs/range_loop.hon | 10 + source/ast/ast.hon | 89 +++++- source/ast/renderer/renderer.hon | 22 ++ source/diagnostics/diagnostics.hon | 10 + source/lexer/scan.hon | 19 +- source/lexer/scan.test.hon | 24 +- source/lexer/token.hon | 3 + source/lower/lower.hon | 89 +++++- source/lower/lower.test.hon | 235 ++++++++++++++++ source/parser/parse.hon | 128 ++++++++- source/parser/parse.test.hon | 117 ++++++++ source/sema/check/check.hon | 103 ++++++- source/sema/check/check.test.hon | 437 +++++++++++++++++++++++++++++ source/sema/resolve.hon | 18 ++ source/sema/resolve.test.hon | 118 ++++++++ 16 files changed, 1416 insertions(+), 16 deletions(-) create mode 100644 programs/range_loop.hon diff --git a/README.md b/README.md index a0abb6c..3524bbd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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). @@ -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. -`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. @@ -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. -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. diff --git a/programs/range_loop.hon b/programs/range_loop.hon new file mode 100644 index 0000000..ee61692 --- /dev/null +++ b/programs/range_loop.hon @@ -0,0 +1,10 @@ +main proc() int { + total := 0 + + for 0..10 |i| { + if (i == 5) continue + total = total + i + } + + return total +} diff --git a/source/ast/ast.hon b/source/ast/ast.hon index 66f0584..3293ec5 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -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) diff --git a/source/ast/renderer/renderer.hon b/source/ast/renderer/renderer.hon index ba4b94b..ae0f707 100644 --- a/source/ast/renderer/renderer.hon +++ b/source/ast/renderer/renderer.hon @@ -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") } } diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index ba96dfa..6bbde15 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -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", diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index bda183f..5d96029 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -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 } diff --git a/source/lexer/scan.test.hon b/source/lexer/scan.test.hon index c08bca0..36556d0 100644 --- a/source/lexer/scan.test.hon +++ b/source/lexer/scan.test.hon @@ -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)) +} diff --git a/source/lexer/token.hon b/source/lexer/token.hon index 36f4c97..62e7fc8 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -42,6 +42,9 @@ TokenKind :: enum { plus, minus star, slash + range, range_inclusive + pipe, at + # logical bang diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 926a9d7..8da4bc6 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -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) diff --git a/source/lower/lower.test.hon b/source/lower/lower.test.hon index c9e45b5..4a85937 100644 --- a/source/lower/lower.test.hon +++ b/source/lower/lower.test.hon @@ -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) + } +} diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 1920cc8..144bc84 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -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) ) } diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index 01e8b10..a1934a2 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -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)) +} diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 28e86c9..f1a54e4 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -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: {} } diff --git a/source/sema/check/check.test.hon b/source/sema/check/check.test.hon index 9ffa8da..177c88f 100644 --- a/source/sema/check/check.test.hon +++ b/source/sema/check/check.test.hon @@ -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)) +} diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index cd2f6ff..3b97264 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -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| { diff --git a/source/sema/resolve.test.hon b/source/sema/resolve.test.hon index 0d6148a..c83ba2c 100644 --- a/source/sema/resolve.test.hon +++ b/source/sema/resolve.test.hon @@ -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)) +}