while loops

This commit is contained in:
2026-06-21 23:26:11 +02:00
parent f4194492cc
commit 380b5943b3
12 changed files with 548 additions and 16 deletions
+175
View File
@@ -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)
}