for loops

This commit is contained in:
2026-06-22 20:11:18 +02:00
parent 380b5943b3
commit 27f42dd253
15 changed files with 1369 additions and 22 deletions
+469
View File
@@ -4080,3 +4080,472 @@ while_loop_allocas_are_emitted_in_the_entry_block :: proc(t: ^testing.T) {
}
testing.expect(t, alloca_count >= 3)
}
@(test)
for_loop_tokens_and_parser_capture_range_shape :: proc(t: ^testing.T) {
text := `main :: func() void {
for 0..4 |value| {
_ = value
}
items [1]mut i32 = [1]
for (&items) |@item, index| {
_ = item
_ = index
}
for 0..=1 |inclusive| {
_ = inclusive
}
}
`
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)
for_count := 0
range_count := 0
inclusive_count := 0
for tok in stream.items {
#partial switch tok.kind {
case .Keyword_For: for_count += 1
case .Range: range_count += 1
case .Range_Inclusive: inclusive_count += 1
case:
}
}
testing.expect_value(t, for_count, 3)
testing.expect_value(t, range_count, 1)
testing.expect_value(t, inclusive_count, 1)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
function := module.functions[0]
first := module.statements[function.body[0]]
second := module.statements[function.body[2]]
third := module.statements[function.body[3]]
testing.expect_value(t, first.kind, ast.Stmt_Kind.For)
testing.expect(t, !first.pointer_capture)
testing.expect_value(t, module.exprs[first.expr].kind, ast.Expr_Kind.Range)
testing.expect_value(t, module.exprs[first.expr].integer, u64(0))
testing.expect_value(t, second.kind, ast.Stmt_Kind.For)
testing.expect(t, second.pointer_capture)
testing.expect(t, symbol.is_valid(second.index_name))
testing.expect_value(t, third.kind, ast.Stmt_Kind.For)
testing.expect_value(t, module.exprs[third.expr].integer, u64(1))
}
@(test)
range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) {
text := `main :: func() void {
limit :: 3
for 0..limit + 1 |bad| {
_ = bad
}
for 0..(limit + 1) |good| {
_ = good
}
}
`
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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found := 0
for diagnostic in diagnostics.items {
found += 1 if strings.contains(diagnostic.message, "range bounds with operators must be parenthesized") else 0
}
testing.expect_value(t, found, 1)
}
@(test)
parser_diagnoses_malformed_for_captures :: proc(t: ^testing.T) {
cases := [4]struct {
text: string,
needle: string,
}{
{`main :: func() void {
for [1] item {}
}
`, "expected '|' before for-loop captures"},
{`main :: func() void {
for [1] |@| {}
}
`, "expected a for-loop item capture"},
{`main :: func() void {
for [1] |item,| {}
}
`, "expected an index capture after ','"},
{`main :: func() void {
for [1] |item {}
}
`, "expected '|' to close for-loop captures"},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
module := parser.parse(&stream, &source_file, &diagnostics)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.needle)
}
testing.expect(t, found)
ast.destroy_module(&module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
for_loops_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-for-loop"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/for_loop", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
range_loop_edges_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-for-loop-edges"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/for_loop_edges", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
for_loop_diagnostics_cover_iterables_captures_and_scope :: proc(t: ^testing.T) {
text := `bad_iterable :: func() void {
for 1 |item| {
_ = item
}
}
bad_array_pointer_capture :: func() void {
for [1] |@item| {
_ = item
}
}
bad_range_pointer_capture :: func() void {
for 0..1 |@item| {
_ = item
}
}
bad_range_index_capture :: func() void {
for 0..1 |item, index| {
_ = item
_ = index
}
}
bad_duplicate_capture :: func() void {
for [1] |item, item| {
_ = item
}
}
bad_capture_redeclaration :: func() void {
for [1] |item| {
item i32 = 2
_ = item
}
}
bad_capture_assignment :: func() void {
for [1] |item| {
item = 2
}
}
bad_immutable_pointer_capture :: func() void {
items :: [1]
for (&items) |@item| {
item^ = 2
}
}
bad_scope :: func() void {
for [1] |item| {
_ = item
}
_ = item
}
bad_integer_bounds :: func() void {
start i32 = 0
end u32 = 1
for start..end |item| {
_ = item
}
}
bad_float_bounds :: func() void {
for 0.0..1.0 |item| {
_ = item
}
}
main :: func() void {
bad_iterable()
bad_array_pointer_capture()
bad_range_pointer_capture()
bad_range_index_capture()
bad_duplicate_capture()
bad_capture_redeclaration()
bad_capture_assignment()
bad_immutable_pointer_capture()
bad_scope()
bad_integer_bounds()
bad_float_bounds()
}
`
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)
unsupported := false
array_pointer := false
range_pointer := false
range_index := false
duplicate_capture := false
redeclaration := false
immutable := false
immutable_pointer := false
scope := false
integer_bounds := 0
for diagnostic in diagnostics.items {
unsupported = unsupported || strings.contains(diagnostic.message, "for-loop iterable must be a range")
array_pointer = array_pointer || strings.contains(diagnostic.message, "pointer capture over an array requires")
range_pointer = range_pointer || strings.contains(diagnostic.message, "range loops do not support pointer captures")
range_index = range_index || strings.contains(diagnostic.message, "range loops do not support index captures")
duplicate_capture = duplicate_capture || strings.contains(diagnostic.message, "for-loop captures must have distinct names")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'item'")
immutable_pointer = immutable_pointer || strings.contains(diagnostic.message, "assignment target is not writable")
scope = scope || strings.contains(diagnostic.message, "unresolved global 'item'")
integer_bounds += 1 if strings.contains(diagnostic.message, "range bounds must be compatible concrete integers") else 0
}
testing.expect(t, unsupported)
testing.expect(t, array_pointer)
testing.expect(t, range_pointer)
testing.expect(t, range_index)
testing.expect(t, duplicate_capture)
testing.expect(t, redeclaration)
testing.expect(t, immutable)
testing.expect(t, immutable_pointer)
testing.expect(t, scope)
testing.expect_value(t, integer_bounds, 2)
}
@(test)
for_pointer_capture_respects_pointer_and_array_mutability :: proc(t: ^testing.T) {
text := `readonly :: func() void {
values [1]mut i32 = [1]
items @[1]mut i32 = &values
items[0] = 7
for items |@item| {
item^ = 7
}
}
writable :: func() void {
values [1]mut i32 = [1]
items @mut [1]mut i32 = &values
items[0] = 7
for items |@item| {
item^ = 7
}
}
main :: func() void {
readonly()
writable()
}
`
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)
readonly_errors := 0
for diagnostic in diagnostics.items {
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
}
testing.expect_value(t, readonly_errors, 2)
}
@(test)
pointer_field_passthrough_respects_pointee_mutability :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
}
readonly :: func(point @Point) i32 {
return point.x
}
bad_write :: func(point @Point) void {
point.x = 7
}
writable :: func(point @mut Point) void {
point.x += 1
}
main :: func() i32 {
point Point = Point { x = 41 }
writable(&point)
bad_write(&point)
return readonly(&point)
}
`
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)
readonly_errors := 0
for diagnostic in diagnostics.items {
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
}
testing.expect_value(t, readonly_errors, 1)
}
@(test)
equal_range_returns_infer_a_usable_result_type :: proc(t: ^testing.T) {
text := `choose :: func(first bool) int {
if first {
return 0..1
}
return 2..3
}
main :: func() i32 {
total i32 = 0
for choose(false) |value| {
total = total + value
}
return total
}
`
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)
found := false
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) == "choose" {
found = true
testing.expect(t, types.is_range(function.result, &hir_module.types))
testing.expect_value(t, types.child_type(function.result, &hir_module.types), types.I8)
}
}
testing.expect(t, found)
testing.expect(t, strings.contains(llvm_text, "extractvalue"))
}
@(test)
for_loop_lowering_evaluates_once_and_avoids_index_bounds_checks :: proc(t: ^testing.T) {
text := `make_range :: func() int {
return 0..2
}
make_array :: func() int {
return [1, 2]
}
main :: func() i32 {
total i32 = 0
for make_range() |value| {
total = total + value
}
for make_array() |value| {
total = total + value
}
return total
}
`
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)
call_count := 0
extract_count := 0
select_count := 0
pointer_add_count := 0
index_address_count := 0
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
testing.expect(t, first_loop_label >= 0)
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction, instruction_index in function.instructions {
#partial switch instruction.op {
case .Call: call_count += 1
case .Extract: extract_count += 1
case .Select: select_count += 1
case .Pointer_Add: pointer_add_count += 1
case .Index_Address: index_address_count += 1
case .Alloca:
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
offset := find_substring_offset(llvm_text, needle)
testing.expect(t, offset >= 0 && offset < first_loop_label)
case:
}
}
}
testing.expect_value(t, call_count, 2)
testing.expect_value(t, extract_count, 3)
testing.expect_value(t, select_count, 2)
testing.expect(t, pointer_add_count >= 1)
testing.expect_value(t, index_address_count, 0)
testing.expect(t, !strings.contains(llvm_text, "index_ok"))
}