From 380b5943b3412a5247dc4b4727ede0db01d2efce Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 21 Jun 2026 23:26:11 +0200 Subject: [PATCH] while loops --- LANGUAGE.md | 16 +-- TODO.md | 68 +++++++++- compiler/ast/ast.odin | 4 + compiler/checker/checker.odin | 73 ++++++++++- compiler/hir/hir.odin | 4 + compiler/lexer/lexer.odin | 4 +- compiler/llvm/llvm.odin | 19 ++- compiler/lower/lower.odin | 54 ++++++++ compiler/parser/parser.odin | 93 ++++++++++++++ compiler/token/token.odin | 2 + compiler_tests.odin | 175 ++++++++++++++++++++++++++ examples/programs/while_loop/main.bro | 52 ++++++++ 12 files changed, 548 insertions(+), 16 deletions(-) create mode 100644 examples/programs/while_loop/main.bro diff --git a/LANGUAGE.md b/LANGUAGE.md index 9499a94..24d0f31 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -12,11 +12,12 @@ ### types and expressions -- exact-width `i8` through `i64`, `u8` through `u64`, `f32`, `f64`, `isize`, `usize`, `void`, and inferred integer-constrained `int` +- exact-width `i8` through `i64`, `u8` through `u64`, `f32`, `f64`, `isize`, `usize`, `bool`, `void`, and inferred integer-constrained `int` - target-dependent atomic C primitives from `c_char` through `c_longdouble` - C primitives remain semantically distinct from exact-width Brolang primitives until target lowering -- contextual integer literals and constant folding of addition and negation trees +- contextual integer and character literals and constant folding of addition and negation trees - strict numeric conversions, checked integer addition, and unary negation +- boolean literals, comparisons, unary `!`, and short-circuiting `and` / `or` - arrays `[N]T`, sentinel arrays `[N;S]T`, single-item pointers `@T`, many-item pointers `*T`, sentinel many-item pointers `[*;S]T`, pointer offsets, slices, and explicit slicing - immutable UTF-8 string literals typed as pointers to static sentinel arrays: `@[N;0]u8` - pointer-to-array `.len`, indexing, and slicing without explicit dereference @@ -24,10 +25,12 @@ - information-preserving and information-forgetting pointer-to-array decay and sentinel slice/pointer weakening; array values never implicitly decay - narrow immutable zero-terminated byte pointer conversion to `*c_char` and `[*;0]c_char`, without general `u8`/`c_char` interchange - optionals with trapping postfix `?`, `orelse`, and nullable pointer representation +- conditional optional unwrapping with immutable then-block bindings: `if value |binding| { ... }` - source-order native structs, defined or opaque `c_struct`, and keyed record literals - complete plain imported C structs and unions as runtime values; incomplete or unsupported-layout records remain pointer-only - C function pointer types as pointer-sized runtime values, including manual `*c_func(...) T` spelling and nullable imported callback typedefs - postfix pointer dereference, general writable locations, function calls, assignments, and returns +- boolean `if` statements and `while` loops with optional post-iteration assignment/expression clauses ### functions and packages @@ -40,6 +43,9 @@ - file-local relative imports, aliases, and qualified member access - relative `.h` imports as synthetic package namespaces - transitive external C function prototypes, typedef chains, C scalars, fixed arrays, complete plain records/unions, and pointers to opaque C records +- imported external C object variables, including writable globals and immutable arrays +- object-like scalar and plain record/union C macro constants +- supported static inline C functions through generated external wrappers - bodyless manual and imported C variadic declarations with target-aware default argument promotions - passing concrete `c_func` declarations/definitions as C callback values and calling non-null C function pointers with postfix call syntax - reference-time diagnostics for unsupported imported C declarations @@ -59,13 +65,9 @@ ### foreign functions and linking - exporting brolang functions to c +- additional target-specific C ABI lowering ### scalar and compound types - tuples and native variadic functions - C enums and non-plain C record layouts - -### advanced c imports - -- C enums, external variables, macros, and static inline functions -- additional target-specific C ABI lowering diff --git a/TODO.md b/TODO.md index 73d1f71..6841bce 100644 --- a/TODO.md +++ b/TODO.md @@ -95,9 +95,12 @@ - new `Optional_Is_Some` / `Optional_Value` IR opcodes (the `Unwrap` presence-test + extract, minus the trap) - conditional unwrapping with guard clause: `if val |v : v >= 10| { ... } else { ... }` - unwrap `val` into `v` if it is not `none` - multi-unwrap (see section below) - - while loops (operates on boolean conditions). examples: + - while loops (implemented; operates on boolean conditions). examples: - `while condition { ... }` - iterate while the condition is true - - `while condition : i += 1 { ... }` - iterate while the condition is true and execute `i += 1` (continue expression) after each iteration + - `while condition : i = i + 1 { ... }` - execute the update after each completed iteration + - the condition and update may be parenthesized independently for visual clarity + - update targets must already be declared and mutable; loops do not introduce implicit induction variables + - compound assignment (`+=`) remains deferred - ranges (see section below) - for loops (operates on iterable sequences). examples: - `for items |item| { ... }` - capture just the `item` value in the array/slice (uses copy semantics, i.e. gets a `T`) @@ -109,8 +112,11 @@ - `for 0..(len) |i| { ... }` or equivalently `for 0..=(len - 1) |i| { ... }` - calculating range bounds, expressions must be parenthesized - for all conditionals/guards, parentheses are optional but allowed for visual clarity -6. compound assignment +6. compound assignment: `+=`, `-=`, `*=`, `/=` +7. enums (native and c interop) (see below) + +8. distinct types (see below) ## A word on multi-unwrap @@ -175,3 +181,59 @@ Ranges represent a sequence of values, commonly used in for loops, and is itself ``` This rule keeps the grammar simple and forces clarity at the call site — no precedence rules to remember. Also, being a value type, ranges can be assigned to variables and passed around like any other value. + +# A word on distinct types + +Distinct types are considered distinct from their backing type. They do not implicitly coerce to their backing type. + +``` +# distinct type +UserID :: distinct u32 + +# instantiate distinct type +my_id UserId :: UserID(42) # value must be of to backing type +``` + +# A word on enums + +``` +# standard enums +Animal :: enum { + dog + cat + bird + lizard +} + +# enums with backing type +Nat :: enum(u8) { # in this case, a maximum of 256 values are possible + one # default: implicitly starts from value 0 + two + three + four + five +} + +# enums with backing type with explicit associated values +# note: must not be jumbled (i.e. `first_val = 1` must come before `other_val = 2`), but is allowed to be discontiguous (i.e. `one = 1` can be followed by `three = 3` without `two = 2` in between) +Nat :: enum(u8) { + one = 1 + two = 2 + three = 3 + # no four + five = 5 +} + +# enums with backing type with semi-implicit associated values +Nat :: enum(u8) { + one = 1 # starts from value 1 + two # implicitly gets value 2 + three # etc... + four + five +} + +# using enums +dog_tag1 Animal :: Animal.dog +dog_tag2 Animal :: .dog # type inferred +``` diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index d309627..45189c0 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -120,6 +120,7 @@ Stmt_Kind :: enum u8 { Return, Expression, If, + While, } Stmt :: struct { @@ -133,8 +134,11 @@ Stmt :: struct { // `If` statements use `expr` as the condition, `body` as the then-block, and // `else_body` as the else-block. An `else if` chain is represented as an // `else_body` holding a single nested `If` statement. + // `While` statements use `expr` as the condition, `body` as the loop body, + // and `update` as the optional post-iteration statement. body: []Stmt_Id, else_body: []Stmt_Id, + update: Stmt_Id, diagnostic: source.Diagnostic_Id, } diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 83612b7..f6c1334 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -631,6 +631,13 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi mark_expr_imports_used(checker, statement.expr, file) mark_block_imports_used(checker, statement.body, file) mark_block_imports_used(checker, statement.else_body, file) + case .While: + mark_expr_imports_used(checker, statement.expr, file) + mark_block_imports_used(checker, statement.body, file) + if statement.update != ast.INVALID_STMT { + update := [1]ast.Stmt_Id{statement.update} + mark_block_imports_used(checker, update[:], file) + } case .Invalid: } } @@ -1424,6 +1431,13 @@ infer_statements :: proc( infer_statements(checker, statement.body, locals, pkg, file, demanded, result) infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result) } + case .While: + _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) + infer_statements(checker, statement.body, locals, pkg, file, demanded, result) + if statement.update != ast.INVALID_STMT { + update := [1]ast.Stmt_Id{statement.update} + infer_statements(checker, update[:], locals, pkg, file, demanded, result) + } } } resize(locals, scope_start) @@ -2460,7 +2474,13 @@ build_expr :: proc( append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) case .Add: stack[frame_index].stage = 1 - append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) + // Preserve assignment/return context for literal operands, e.g. + // assigning `i + 1` back into a `u32` local. + left_expected := types.INVALID + if types.is_concrete_scalar(frame.expected) && !types.is_bool(frame.expected) { + left_expected = frame.expected + } + append(&stack, Build_Expr_Frame{expr=expr.left, expected=left_expected, template=ast.INVALID_FUNCTION}) case .Call: if expr.left != ast.INVALID_EXPR { stack[frame_index].stage = 6 @@ -2600,6 +2620,12 @@ build_expr :: proc( right_expected := types.INVALID if types.is_many_pointer(checker.module.exprs[last].type, &checker.module.types) { right_expected = types.USIZE + } else if eval_constant(checker, expr.right).kind == .Value { + // A constant RHS adopts the concrete LHS type before numeric + // compatibility is checked. + right_expected = checker.module.exprs[last].type + } else if types.is_concrete_scalar(frame.expected) && !types.is_bool(frame.expected) { + right_expected = frame.expected } append(&stack, Build_Expr_Frame{expr=expr.right, expected=right_expected, template=ast.INVALID_FUNCTION}) continue @@ -3067,6 +3093,39 @@ build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id diagnostic = source.INVALID_DIAGNOSTIC, }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid + case .While: + condition := build_expr( + checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + types.BOOL, ctx.pkg, ctx.file, + ) + if checker.module.exprs[condition].kind != .Invalid && + !types.is_bool(checker.module.exprs[condition].type) { + id := source.add(checker.diagnostics, statement.span, "'while' condition must be a bool") + condition = invalid_hir_expr(checker, statement.span, id, types.BOOL) + ctx.problematic^ = true + } + loop_body := build_block(ctx, statement.body) + update := hir.INVALID_STMT + if statement.update != ast.INVALID_STMT { + update_ast := [1]ast.Stmt_Id{statement.update} + update_body := build_block(ctx, update_ast[:]) + if len(update_body) > 0 { + update = update_body[0] + } + delete(update_body, checker.allocator) + } + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind=.While, + span=statement.span, + expr=condition, + then_body=loop_body, + update=update, + local=hir.INVALID_LOCAL, + target=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid case .Invalid: append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ @@ -3082,8 +3141,9 @@ build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id // Reports whether every control-flow path through `stmts` terminates (returns or traps), // so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an -// `.If` terminates only when it has an `else` and both arms terminate. Recursion into the -// `then_body`/`else_body` slices handles nested ifs and `else if` chains. +// `.If` terminates only when it has an `else` and both arms terminate. A literal +// `while true` cannot fall through because the language has no `break` statement. +// Recursion into the branch slices handles nested ifs and `else if` chains. all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { for id in stmts { statement := module.statements[id] @@ -3096,6 +3156,13 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { all_paths_return(module, statement.else_body) { return true } + case .While: + if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) { + condition := module.exprs[statement.expr] + if condition.kind == .Bool && condition.integer != 0 { + return true + } + } } } return false diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 8974f78..d6f408f 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -141,6 +141,7 @@ Stmt_Kind :: enum u8 { Sink, Trap, If, + While, } Stmt :: struct { @@ -151,8 +152,11 @@ Stmt :: struct { expr: Expr_Id, // `If` statements use `expr` as the condition and `then_body`/`else_body` as // the branch statement lists. + // `While` statements use `expr` as the condition, `then_body` as the loop + // body, and `update` as the optional post-iteration statement. then_body: []Stmt_Id, else_body: []Stmt_Id, + update: Stmt_Id, diagnostic: source.Diagnostic_Id, } diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 9326225..ea6f271 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -26,6 +26,7 @@ keyword_kind :: proc(text: string) -> token.Kind { case "and": return .Keyword_And case "or": return .Keyword_Or case "if": return .Keyword_If + case "while": return .Keyword_While case "else": return .Keyword_Else case "true": return .Keyword_True case "false": return .Keyword_False @@ -109,8 +110,7 @@ lex :: proc( cursor += 1 append_token(&stream, source_file, .Colon_Colon, start, cursor) } else { - id := source.add(diagnostics, source.Span{file=source_file.id, start=source.Offset(start), end=source.Offset(cursor)}, "expected a second ':'") - append_token(&stream, source_file, .Invalid, start, cursor, diagnostic=id) + append_token(&stream, source_file, .Colon, start, cursor) } case '=': start := cursor diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 2b692d8..45c9f5a 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -497,6 +497,20 @@ float_predicate :: proc(predicate: ir.Compare_Predicate) -> string { return "oeq" } +emit_entry_allocas :: proc(emitter: ^Emitter, instructions: []ir.Instruction) { + for instruction, instruction_index in instructions { + if instruction.op == .Alloca && + types.is_runtime_value(instruction.type, &emitter.module.types) { + fmt.sbprintf( + &emitter.builder, + " %%v%d = alloca %s\n", + instruction_index, + llvm_type(instruction.type, &emitter.module.types), + ) + } + } +} + emit_instruction_stream :: proc( emitter: ^Emitter, instructions: []ir.Instruction, @@ -706,7 +720,9 @@ emit_instruction_stream :: proc( emit_recovery_value(emitter, instruction_index, instruction, "invalid allocation type") continue } - fmt.sbprintf(&emitter.builder, " %%v%d = alloca %s\n", instruction_index, llvm_type(instruction.type, &emitter.module.types)) + if global_initializer { + fmt.sbprintf(&emitter.builder, " %%v%d = alloca %s\n", instruction_index, llvm_type(instruction.type, &emitter.module.types)) + } case .Index_Address: if !valid_instruction(instructions, instruction.a) || !valid_value(instructions, instruction.b, types.USIZE, &emitter.module.types) { @@ -1726,6 +1742,7 @@ emit_functions :: proc(emitter: ^Emitter) { continue } strings.write_string(&emitter.builder, ") {\nentry:\n") + emit_entry_allocas(emitter, function.instructions) if function.calling_convention == .C { for param_type, index in function.param_types { if !types.is_record(param_type, &emitter.module.types) { diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 9a6e5a5..6d8143f 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -708,6 +708,60 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) + case .While: + condition_lbl := fresh_label(state) + body_lbl := fresh_label(state) + exit_lbl := fresh_label(state) + update_lbl := condition_lbl + if statement.update != hir.INVALID_STMT { + update_lbl = fresh_label(state) + } + append_instruction(state, ir.Instruction{ + op=.Br, span=statement.span, type=types.VOID, integer=condition_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + append_instruction(state, ir.Instruction{ + op=.Label, span=statement.span, type=types.VOID, integer=condition_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + condition := lower_expr(state, statement.expr) + append_instruction(state, ir.Instruction{ + op=.Cond_Br, span=statement.span, type=types.VOID, + a=condition, integer=body_lbl, target=ir.Ref(u32(exit_lbl)), + b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) + append_instruction(state, ir.Instruction{ + op=.Label, span=statement.span, type=types.VOID, integer=body_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + lower_statements(state, statement.then_body) + append_instruction(state, ir.Instruction{ + op=.Br, span=statement.span, type=types.VOID, integer=update_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + if statement.update != hir.INVALID_STMT { + append_instruction(state, ir.Instruction{ + op=.Label, span=statement.span, type=types.VOID, integer=update_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + update := [1]hir.Stmt_Id{statement.update} + lower_statements(state, update[:]) + append_instruction(state, ir.Instruction{ + op=.Br, span=statement.span, type=types.VOID, integer=condition_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + append_instruction(state, ir.Instruction{ + op=.Label, span=statement.span, type=types.VOID, integer=exit_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) } } } diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index b45192e..c7b8bff 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -928,6 +928,9 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { if current(parser).kind == .Keyword_If { return parse_if(parser) } + if current(parser).kind == .Keyword_While { + return parse_while(parser) + } if current(parser).kind == .Identifier || current(parser).kind == .Underscore { start_cursor := parser.cursor @@ -1123,6 +1126,96 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { return id } +parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id { + parenthesized := false + if _, ok := allow(parser, .Left_Paren); ok { + parenthesized = true + parser.delimiter_depth += 1 + skip_newlines(parser) + } + + update := ast.INVALID_STMT + if current(parser).kind == .Left_Brace || + current(parser).kind == .Right_Paren || + current(parser).kind == .Eof { + diagnostic := source.add( + parser.diagnostics, + current(parser).span, + "expected a while update statement after ':'", + ) + update = ast.stmt_id(len(parser.module.statements)) + append(&parser.module.statements, ast.Stmt{ + kind=.Invalid, + span=current(parser).span, + expr=ast.INVALID_EXPR, + update=ast.INVALID_STMT, + diagnostic=diagnostic, + }) + } else { + saved := parser.no_struct_literal + parser.no_struct_literal = true + update = parse_statement(parser) + parser.no_struct_literal = saved + statement := &parser.module.statements[update] + switch statement.kind { + case .Assignment, .Expression: + case .Invalid, .Declaration, .Return, .If, .While: + diagnostic := source.add( + parser.diagnostics, + statement.span, + "while update must be an assignment, sink, or expression statement", + ) + statement.kind = .Invalid + statement.diagnostic = diagnostic + } + } + + if parenthesized { + skip_newlines(parser) + if _, ok := allow(parser, .Right_Paren); !ok { + source.add(parser.diagnostics, current(parser).span, "expected ')' after while update") + for current(parser).kind != .Right_Paren && + current(parser).kind != .Left_Brace && + current(parser).kind != .Newline && + current(parser).kind != .Eof { + advance(parser) + } + _, _ = allow(parser, .Right_Paren) + } + parser.delimiter_depth -= 1 + } + return update +} + +parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id { + start := advance(parser) // consume 'while' + skip_newlines(parser) + saved := parser.no_struct_literal + parser.no_struct_literal = true + condition := parse_expression(parser) + parser.no_struct_literal = saved + skip_newlines(parser) + + update := ast.INVALID_STMT + if _, ok := allow(parser, .Colon); ok { + skip_newlines(parser) + update = parse_while_update(parser) + skip_newlines(parser) + } + body := parse_block(parser) + + id := ast.stmt_id(len(parser.module.statements)) + append(&parser.module.statements, ast.Stmt{ + kind=.While, + span=span_from(start.span, previous(parser).span), + expr=condition, + body=body, + update=update, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id +} + parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { advance(parser) if _, ok := allow(parser, .Left_Paren); !ok { diff --git a/compiler/token/token.odin b/compiler/token/token.odin index cc71fd2..921a7c4 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -13,6 +13,7 @@ Kind :: enum u8 { String, Character, Underscore, + Colon, Colon_Colon, Equal, Equal_Equal, @@ -53,6 +54,7 @@ Kind :: enum u8 { Keyword_And, Keyword_Or, Keyword_If, + Keyword_While, Keyword_Else, Keyword_True, Keyword_False, diff --git a/compiler_tests.odin b/compiler_tests.odin index 2f337dc..8001b9d 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2293,6 +2293,18 @@ count_substring_occurrences :: proc(text, needle: string) -> int { return count } +find_substring_offset :: proc(text, needle: string) -> int { + if len(needle) == 0 { + return 0 + } + for index := 0; index + len(needle) <= len(text); index += 1 { + if text[index:index + len(needle)] == needle { + return index + } + } + return -1 +} + find_cimport_variable :: proc(result: ^cimport.Result, name: string) -> (^cimport.Variable, bool) { for &variable in result.variables { if variable.name == name { @@ -3905,3 +3917,166 @@ if_unwrap_binding_is_immutable :: proc(t: ^testing.T) { } testing.expect(t, found) } + +@(test) +while_loops_compile_and_run :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-while-loop" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/while_loop", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 42) +} + +@(test) +while_loop_diagnostics_cover_condition_update_and_scope :: proc(t: ^testing.T) { + text := `bad_condition :: func() void { + while 1 {} +} +bad_unresolved :: func() void { + while false : missing = 1 {} +} +bad_immutable :: func() void { + i :: 0 + while false : i = i + 1 {} +} +bad_body_scope :: func() void { + running :: false + while running : i = 1 { + i u32 = 0 + } +} +bad_declaration_update :: func() void { + while false : i u32 = 0 {} +} +bad_missing_update :: func() void { + while false : {} +} +main :: func() void { + bad_condition() + bad_unresolved() + bad_immutable() + bad_body_scope() + bad_declaration_update() + bad_missing_update() +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + non_bool := false + unresolved_missing := false + unresolved_body_local := false + immutable := false + disallowed := false + missing := false + for diagnostic in diagnostics.items { + non_bool = non_bool || strings.contains(diagnostic.message, "'while' condition must be a bool") + unresolved_missing = unresolved_missing || strings.contains(diagnostic.message, "cannot assign unresolved local 'missing'") + unresolved_body_local = unresolved_body_local || strings.contains(diagnostic.message, "cannot assign unresolved local 'i'") + immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'i'") + disallowed = disallowed || strings.contains(diagnostic.message, "while update must be an assignment, sink, or expression statement") + missing = missing || strings.contains(diagnostic.message, "expected a while update statement after ':'") + } + testing.expect(t, non_bool) + testing.expect(t, unresolved_missing) + testing.expect(t, unresolved_body_local) + testing.expect(t, immutable) + testing.expect(t, disallowed) + testing.expect(t, missing) +} + +@(test) +while_true_and_potential_fallthrough_have_distinct_return_analysis :: proc(t: ^testing.T) { + text := `forever :: func() i32 { + while true {} +} +maybe :: func(run bool) i32 { + while run { + return 1 + } +} +main :: func() void { + if false { + _ = forever() + } + _ = maybe(false) +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + missing_return_count := 0 + for diagnostic in diagnostics.items { + if strings.contains(diagnostic.message, "does not return a value") { + missing_return_count += 1 + } + } + testing.expect_value(t, missing_return_count, 1) +} + +@(test) +while_loop_allocas_are_emitted_in_the_entry_block :: proc(t: ^testing.T) { + text := `main :: func() i32 { + i u32 = 0 + while i < 2 and true : i = i + 1 { + value u32 = i + _ = value + } + return 0 +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + + testing.expect_value(t, len(diagnostics.items), 0) + first_loop_label := find_substring_offset(llvm_text, "bro_block_") + testing.expect(t, first_loop_label >= 0) + alloca_count := 0 + for function in ir_module.functions { + if !function.is_main { + continue + } + for instruction, instruction_index in function.instructions { + if instruction.op != .Alloca { + continue + } + alloca_count += 1 + needle := fmt.tprintf(" %%v%d = alloca ", instruction_index) + offset := find_substring_offset(llvm_text, needle) + testing.expect(t, offset >= 0 && offset < first_loop_label) + } + } + testing.expect(t, alloca_count >= 3) +} diff --git a/examples/programs/while_loop/main.bro b/examples/programs/while_loop/main.bro new file mode 100644 index 0000000..8734b52 --- /dev/null +++ b/examples/programs/while_loop/main.bro @@ -0,0 +1,52 @@ +# Milestone 5: boolean while loops with optional post-iteration updates. + +return_before_update :: func() i32 { + i i32 = 0 + while true : i = i + 1 { + return i + } +} + +main :: func() i32 { + total i32 = 0 + + # ordinary condition and update + i u32 = 0 + while i < 5 : i = i + 1 { + total = total + 2 + } + + # equivalent parenthesized header + j u32 = 0 + while (j < 4) : (j = j + 1) { + total = total + 3 + } + + # nested loops and body-local storage + outer u32 = 0 + while outer < 2 : outer = outer + 1 { + inner u32 = 0 + while inner < 3 : inner = inner + 1 { + total = total + 2 + } + } + + # The body-local k shadows only inside the body. The update still targets + # the mutable k declared before the loop. + k u32 = 0 + while k < 4 : k = k + 1 { + k u32 = 100 + if k == 100 { + total = total + 2 + } + } + + # zero iterations + while false { + total = total + 100 + } + + # A return exits before the update clause. + total = total + return_before_update() + return total +}