for loops
This commit is contained in:
@@ -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"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user