package main import compiler_core "./compiler" import "./compiler/ast" import "./compiler/backend" import "./compiler/cimport" import "./compiler/checker" import "./compiler/hir" import "./compiler/ir" import "./compiler/lexer" import "./compiler/linker" import "./compiler/loader" import "./compiler/llvm" import "./compiler/lower" import "./compiler/parser" import "./compiler/source" import "./compiler/symbol" import "./compiler/target" import "./compiler/token" import "./compiler/translatec" import "./compiler/types" import "core:fmt" import "core:mem" import "core:os" import "core:os/os2" import "core:strings" import "core:testing" @(test) symbol_table_deduplicates_and_owns_spellings :: proc(t: ^testing.T) { symbols := symbol.init_table() defer symbol.destroy_table(&symbols) buffer := [5]byte{'a', 'l', 'p', 'h', 'a'} alpha := symbol.intern(&symbols, string(buffer[:])) duplicate := symbol.intern(&symbols, "alpha") beta := symbol.intern(&symbols, "beta") buffer[0] = 'x' testing.expect_value(t, alpha, duplicate) testing.expect(t, alpha != beta) testing.expect_value(t, symbol.resolve(&symbols, alpha), "alpha") testing.expect_value(t, symbol.resolve(&symbols, beta), "beta") testing.expect_value(t, symbol.intern(&symbols, ""), symbol.INVALID) testing.expect_value(t, symbol.resolve(&symbols, symbol.INVALID), "") testing.expect(t, !symbol.is_valid(symbol.INVALID)) testing.expect(t, symbol.is_valid(alpha)) } @(test) compact_tokens_intern_only_identifiers_and_preserve_parser_text :: proc(t: ^testing.T) { text := `other :: import "../math" value :: 42 main func() void { _ = value } ` 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) value_symbol := symbol.intern(&symbols, "value") sink_symbol := symbol.intern(&symbols, "_") value_count := 0 for tok in stream.items { #partial switch tok.kind { case .Identifier: if tok.symbol == value_symbol { value_count += 1 } case .Underscore: testing.expect_value(t, tok.symbol, sink_symbol) case: testing.expect_value(t, tok.symbol, symbol.INVALID) } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, value_count, 2) testing.expect_value(t, module.imports[0].path, "../math") testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(42)) testing.expect_value(t, module.statements[module.functions[0].body[0]].name, sink_symbol) } @(test) compact_ids_reserve_invalid_values_and_preserve_layout :: proc(t: ^testing.T) { testing.expect_value(t, size_of(source.Span), 12) testing.expect_value(t, size_of(token.Token), 24) testing.expect(t, size_of(ast.Expr) <= 88) testing.expect(t, size_of(hir.Expr) <= 88) testing.expect(t, size_of(ir.Instruction) <= 88) testing.expect_value(t, size_of(types.Type), 4) source_index, source_ok := source.source_index(source.Source_Id(0), 1) testing.expect_value(t, source_index, 0) testing.expect(t, source_ok) _, source_invalid := source.source_index(source.INVALID_SOURCE, 1) testing.expect(t, !source_invalid) _, source_out_of_bounds := source.source_index(source.Source_Id(1), 1) testing.expect(t, !source_out_of_bounds) diagnostic_index, diagnostic_ok := source.diagnostic_index(source.Diagnostic_Id(0), 1) testing.expect_value(t, diagnostic_index, 0) testing.expect(t, diagnostic_ok) _, diagnostic_invalid := source.diagnostic_index(source.INVALID_DIAGNOSTIC, 1) testing.expect(t, !diagnostic_invalid) expr_index, expr_ok := ast.index(ast.Expr_Id(0), ast.INVALID_EXPR, 1) testing.expect_value(t, expr_index, 0) testing.expect(t, expr_ok) _, expr_invalid := ast.index(ast.INVALID_EXPR, ast.INVALID_EXPR, 1) testing.expect(t, !expr_invalid) function_index, function_ok := hir.index(hir.Function_Id(0), hir.INVALID_FUNCTION, 1) testing.expect_value(t, function_index, 0) testing.expect(t, function_ok) _, function_invalid := hir.index(hir.INVALID_FUNCTION, hir.INVALID_FUNCTION, 1) testing.expect(t, !function_invalid) instruction_index, instruction_ok := ir.index(ir.Instruction_Id(0), ir.INVALID_INSTRUCTION, 1) testing.expect_value(t, instruction_index, 0) testing.expect(t, instruction_ok) _, instruction_invalid := ir.index(ir.INVALID_INSTRUCTION, ir.INVALID_INSTRUCTION, 1) testing.expect(t, !instruction_invalid) spec_index, spec_ok := checker.spec_index(checker.Spec_Id(0), 1) testing.expect_value(t, spec_index, 0) testing.expect(t, spec_ok) _, spec_invalid := checker.spec_index(checker.INVALID_SPEC, 1) testing.expect(t, !spec_invalid) testing.expect(t, source.fits_source_length(u64(0xffff_ffff))) testing.expect(t, !source.fits_source_length(u64(0x1_0000_0000))) } @(test) lexer_preserves_newlines_and_skips_comments :: proc(t: ^testing.T) { source_file := source.Source{path="test.bro", text="# comment\nmain func() void {}\n"} 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, stream.items[0].kind, token.Kind.Newline) testing.expect_value(t, stream.items[1].kind, token.Kind.Identifier) } @(test) parser_accepts_grouped_params_and_multiline_statements :: proc(t: ^testing.T) { text := `sum func(a, b int) int { return (a + b) } main func() void {} ` 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(module.functions), 2) testing.expect_value(t, len(module.functions[0].params), 2) } @(test) parser_accepts_sentinel_many_item_pointer_types :: proc(t: ^testing.T) { text := `zero func(value [*;0]u8) void {} newline func(value [*;'\n']mut u8) void {} nullable func(value ?[*;0]u8) void {} main func() void {} ` 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) zero, zero_ok := types.node(&module.type_store, module.functions[0].params[0].type) newline, newline_ok := types.node(&module.type_store, module.functions[1].params[0].type) nullable, nullable_ok := types.node(&module.type_store, module.functions[2].params[0].type) nullable_child, nullable_child_ok := types.node(&module.type_store, nullable.child) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, zero_ok && zero.kind == .Pointer && zero.many && zero.has_sentinel && zero.sentinel == 0) testing.expect(t, newline_ok && newline.kind == .Pointer && newline.many && newline.mutable && newline.has_sentinel && newline.sentinel == '\n') testing.expect(t, nullable_ok && nullable.kind == .Optional) testing.expect(t, nullable_child_ok && nullable_child.kind == .Pointer && nullable_child.many && nullable_child.has_sentinel) } @(test) parser_accepts_c_function_pointer_types :: proc(t: ^testing.T) { text := `take c_func(callback ?*c_func(value c_int) c_int) void main func() void {} ` 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) optional, optional_ok := types.node(&module.type_store, module.functions[0].params[0].type) pointer, pointer_ok := types.node(&module.type_store, optional.child) function, function_ok := types.node(&module.type_store, pointer.child) params := types.params_for(&module.type_store, pointer.child) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, optional_ok && optional.kind == .Optional) testing.expect(t, pointer_ok && pointer.kind == .Pointer && pointer.many && !pointer.mutable) testing.expect(t, function_ok && function.kind == .Function && function.c_abi && !function.variadic) testing.expect(t, function.child == types.C_INT) testing.expect_value(t, len(params), 1) testing.expect(t, params[0].type == types.C_INT) } @(test) parser_accepts_c_function_pointer_alias_types :: proc(t: ^testing.T) { text := `callback_alias :: alias ?*c_func(value i32) i32 main func() void {} ` 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) name := symbol.intern(&symbols, "callback_alias") alias := types.find_named(&module.type_store, 0, u32(name)) alias_node, alias_ok := types.node(&module.type_store, alias) optional, optional_ok := types.node(&module.type_store, alias_node.child) pointer, pointer_ok := types.node(&module.type_store, optional.child) function, function_ok := types.node(&module.type_store, pointer.child) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, alias_ok && alias_node.kind == .Alias) testing.expect(t, optional_ok && optional.kind == .Optional) testing.expect(t, pointer_ok && pointer.kind == .Pointer) testing.expect(t, function_ok && function.kind == .Function && function.c_abi) } @(test) parser_rejects_old_function_declaration_binding_syntax :: proc(t: ^testing.T) { text := `main :: func() void {} foreign :: c_func() i32 ` 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 { if strings.contains(diagnostic.message, "function declarations do not use '::'") { found += 1 } } testing.expect_value(t, found, 2) testing.expect_value(t, len(module.functions), 2) } @(test) parser_diagnoses_malformed_sentinel_pointer_types :: proc(t: ^testing.T) { text := `bad func(value [*0]u8) void {} main func() void {} ` 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 := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "expected ';' after '*' in sentinel pointer type") } testing.expect(t, found) } @(test) parser_accepts_single_statement_one_line_functions :: proc(t: ^testing.T) { text := `give func() i8 { return 7 } main func() void { _ = give() } ` 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(module.functions), 2) testing.expect_value(t, len(module.functions[0].body), 1) testing.expect_value(t, len(module.functions[1].body), 1) testing.expect_value(t, module.statements[module.functions[0].body[0]].kind, ast.Stmt_Kind.Return) testing.expect_value(t, module.statements[module.functions[1].body[0]].kind, ast.Stmt_Kind.Assignment) } @(test) parser_distinguishes_bodyless_declarations_and_definitions :: proc(t: ^testing.T) { text := `foreign c_func(value i32) i32 defined c_func(value i32) i32 { return value } native func() i32 main func() void {} ` 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(module.functions), 4) testing.expect(t, !module.functions[0].has_body) testing.expect(t, module.functions[0].c_abi) testing.expect(t, module.functions[1].has_body) testing.expect(t, module.functions[1].c_abi) testing.expect_value(t, len(module.functions[1].body), 1) testing.expect(t, !module.functions[2].has_body) testing.expect(t, !module.functions[2].c_abi) testing.expect(t, module.functions[3].has_body) } @(test) parser_accepts_terminal_c_variadic_markers_and_recovers_nonterminal_markers :: proc(t: ^testing.T) { text := `fixed c_func(value c_int, ...) c_int zero c_func(...) void bad c_func(..., value c_int) c_int main func() void {} ` 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_ellipsis := false for tok in stream.items { found_ellipsis = found_ellipsis || tok.kind == .Ellipsis } testing.expect(t, found_ellipsis) testing.expect(t, module.functions[0].variadic) testing.expect_value(t, len(module.functions[0].params), 1) testing.expect(t, module.functions[1].variadic) testing.expect_value(t, len(module.functions[1].params), 0) testing.expect(t, module.functions[2].variadic) testing.expect_value(t, len(module.functions[2].params), 1) testing.expect_value(t, len(diagnostics.items), 1) testing.expect(t, strings.contains(diagnostics.items[0].message, "final parameter")) } @(test) parser_treats_c_as_contextual_only_before_func :: proc(t: ^testing.T) { text := `c :: 5 x :: c foreign c_func() i32 broken :: c 5 main func() void {} ` 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) c_symbol := symbol.intern(&symbols, "c") for tok in stream.items { if int(tok.span.start) < len(text) && text[int(tok.span.start):int(tok.span.end)] == "c" { testing.expect_value(t, tok.kind, token.Kind.Identifier) testing.expect_value(t, tok.symbol, c_symbol) } } testing.expect_value(t, len(module.globals), 3) testing.expect_value(t, module.exprs[module.globals[1].expr].name, c_symbol) testing.expect_value(t, module.exprs[module.globals[2].expr].name, c_symbol) testing.expect(t, module.functions[0].c_abi) testing.expect_value(t, len(diagnostics.items), 1) testing.expect(t, strings.contains(diagnostics.items[0].message, "followed by a newline")) } @(test) parser_accepts_undefined_expression :: proc(t: ^testing.T) { text := `main func() void { value i32 = undefined } ` 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_keyword := false for tok in stream.items { found_keyword = found_keyword || tok.kind == .Keyword_Undefined } statement := module.statements[module.functions[0].body[0]] testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found_keyword) testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Undefined) } @(test) pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) { source_file := source.Source{path="test.bro", text="value :: 1 + 2 + 3\nmain func() void {}\n"} 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) root := module.exprs[module.globals[0].expr] testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, root.kind, ast.Expr_Kind.Add) testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Add) testing.expect_value(t, module.exprs[root.right].integer, u64(3)) } @(test) pratt_parser_handles_prefix_negation_precedence :: proc(t: ^testing.T) { text := `identity func(value i8) i8 { return value } loose :: -1 + 2 grouped :: -(1 + 2) called :: -identity(1) chained :: --1 main func() void {} ` 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) loose := module.exprs[module.globals[0].expr] grouped := module.exprs[module.globals[1].expr] called := module.exprs[module.globals[2].expr] chained := module.exprs[module.globals[3].expr] testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, loose.kind, ast.Expr_Kind.Add) testing.expect_value(t, module.exprs[loose.left].kind, ast.Expr_Kind.Negate) testing.expect_value(t, grouped.kind, ast.Expr_Kind.Negate) testing.expect_value(t, module.exprs[grouped.left].kind, ast.Expr_Kind.Add) testing.expect_value(t, called.kind, ast.Expr_Kind.Negate) testing.expect_value(t, module.exprs[called.left].kind, ast.Expr_Kind.Call) testing.expect_value(t, chained.kind, ast.Expr_Kind.Negate) testing.expect_value(t, module.exprs[chained.left].kind, ast.Expr_Kind.Negate) } nested_expression_source :: proc(call: bool, depth: int) -> string { builder := strings.builder_make() defer strings.builder_destroy(&builder) if call { strings.write_string(&builder, "identity func(value i32) i32 { return value }\nvalue :: ") for _ in 0.. string { builder := strings.builder_make() defer strings.builder_destroy(&builder) strings.write_string(&builder, "value :: ") for _ in 0.. (count: int, found_budget: bool) { 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) for diagnostic in diagnostics.items { found_budget = found_budget || strings.contains(diagnostic.message, "expression nesting exceeds 256 levels") } return len(diagnostics.items), found_budget } @(test) parser_enforces_explicit_expression_nesting_budget :: proc(t: ^testing.T) { modes := [?]bool{false, true} for call in modes { at_limit := nested_expression_source(call, parser.MAX_EXPRESSION_NESTING) defer delete(at_limit) count, found := parse_nesting_result(at_limit) testing.expect_value(t, count, 0) testing.expect(t, !found) over_limit := nested_expression_source(call, parser.MAX_EXPRESSION_NESTING+1) defer delete(over_limit) _, found = parse_nesting_result(over_limit) testing.expect(t, found) } at_limit := nested_negation_source(parser.MAX_EXPRESSION_NESTING) defer delete(at_limit) count, found := parse_nesting_result(at_limit) testing.expect_value(t, count, 0) testing.expect(t, !found) over_limit := nested_negation_source(parser.MAX_EXPRESSION_NESTING+1) defer delete(over_limit) _, found = parse_nesting_result(over_limit) testing.expect(t, found) } @(test) cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T) { options, valid := parse_cli_args([]string{ "brolang", "app", "--c-link", "native.c", "-o", "app.out", "--c-library-path", "vendor/lib", "--c-library", "thing", "--c-link", "helper.o", "--c-include-path", "vendor/include", "--c-define", "FEATURE=1", "--target", "aarch64-macos", }) defer delete(options.link_arguments) defer delete(options.c_options.include_paths) defer delete(options.c_options.defines) testing.expect(t, valid) testing.expect_value(t, options.input_path, "app") testing.expect_value(t, options.output_path, "app.out") testing.expect_value(t, len(options.link_arguments), 4) testing.expect_value(t, options.link_arguments[0].kind, linker.Kind.Input) testing.expect_value(t, options.link_arguments[0].value, "native.c") testing.expect_value(t, options.link_arguments[1].kind, linker.Kind.Library_Path) testing.expect_value(t, options.link_arguments[2].kind, linker.Kind.Library) testing.expect_value(t, options.link_arguments[3].value, "helper.o") testing.expect_value(t, options.c_options.include_paths[0], "vendor/include") testing.expect_value(t, options.c_options.defines[0], "FEATURE=1") testing.expect_value(t, target.name(options.target), "aarch64-macos") _, unknown_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--unknown", "value"}) _, incomplete_valid := parse_cli_args([]string{"brolang", "app", "-o"}) _, duplicate_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "one", "-o", "two"}) _, duplicate_empty_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "", "-o", "two"}) _, invalid_target := parse_cli_args([]string{"brolang", "app", "-o", "out", "--target", "x86_64-linux"}) _, legacy_link := parse_cli_args([]string{"brolang", "app", "-o", "out", "--link", "native.c"}) _, legacy_library_path := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library-path", "vendor/lib"}) _, legacy_library := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library", "thing"}) testing.expect(t, !unknown_valid) testing.expect(t, !incomplete_valid) testing.expect(t, !duplicate_output_valid) testing.expect(t, !duplicate_empty_output_valid) testing.expect(t, !invalid_target) testing.expect(t, !legacy_link) testing.expect(t, !legacy_library_path) testing.expect(t, !legacy_library) } @(test) parser_accepts_bare_and_aliased_imports :: proc(t: ^testing.T) { text := `import "../math" other :: import "../math" escaped :: import "dir\"name\\tail" main func() void {} ` 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(module.imports), 3) testing.expect_value(t, symbol.resolve(&symbols, module.imports[0].alias), "") testing.expect_value(t, module.imports[0].path, "../math") testing.expect_value(t, symbol.resolve(&symbols, module.imports[1].alias), "other") testing.expect_value(t, module.imports[2].path, "dir\"name\\tail") } @(test) parser_accepts_chained_field_access :: proc(t: ^testing.T) { text := `main func() void { _ = first.second.value } ` 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) testing.expect_value(t, len(diagnostics.items), 0) } @(test) lexer_diagnoses_invalid_import_strings :: proc(t: ^testing.T) { text := "import \"bad\\q\"\nimport \"unterminated\n" 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) testing.expect_value(t, len(diagnostics.items), 2) } @(test) package_loader_discovers_lexical_immediate_bro_files :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) module, loaded := loader.load("examples/packages/basic/app", &sources, &diagnostics, &symbols) defer ast.destroy_module(&module) testing.expect(t, loaded) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(module.packages), 2) testing.expect_value(t, len(module.files), 3) testing.expect(t, strings.has_suffix(sources.items[module.files[0].source].path, "/main.bro")) testing.expect(t, strings.has_suffix(sources.items[module.files[1].source].path, "/value.bro")) testing.expect(t, strings.has_suffix(sources.items[module.files[2].source].path, "/math.bro")) } @(test) multi_source_diagnostics_report_the_originating_file :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) module, loaded := loader.load("examples/packages/file_local/app", &sources, &diagnostics, &symbols) defer ast.destroy_module(&module) hir_module := checker.check(&module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) testing.expect(t, loaded) found := false for _, diagnostic_index in diagnostics.items { message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index)) if strings.contains(message, "/b.bro:2:9:") && strings.contains(message, "unknown package alias 'math'") { found = true } delete(message) } testing.expect(t, found) } @(test) semantic_lookup_diagnoses_wrong_declaration_kinds :: proc(t: ^testing.T) { text := `value :: 1 give func() i8 { return 1 } main func() void { _ = value() _ = give } ` 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) found_global := false found_function := false for diagnostic in diagnostics.items { found_global = found_global || strings.contains(diagnostic.message, "'value' is a global, not a function") found_function = found_function || strings.contains(diagnostic.message, "'give' is a function, not a global value") } testing.expect(t, found_global) testing.expect(t, found_function) } @(test) pipeline_emits_specialized_calling_conventions_and_checked_add :: proc(t: ^testing.T) { text := `sum_c c_func(a, b int) int { return a + b } sum_bro func(a, b int) int { return a + b } main func() void { _ = sum_c(1, 2) _ = sum_bro(1, 2) } ` 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) second_llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) defer delete(second_llvm_text) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, llvm_text, second_llvm_text) testing.expect(t, strings.contains(llvm_text, "define signext i8 @bro_c__p0__sum_c__i8__i8")) testing.expect(t, strings.contains(llvm_text, "define internal fastcc i8 @bro__p0__sum_bro__i8__i8")) testing.expect(t, strings.contains(llvm_text, "@llvm.sadd.with.overflow.i8")) } @(test) pipeline_emits_only_referenced_foreign_declarations_with_exact_names :: proc(t: ^testing.T) { text := `used c_func(a, b i32) i32 unused c_func() i32 bodyful c_func(value i32) i32 { return value } main func() void { _ = used(1, 2) _ = bodyful(3) } ` 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) testing.expect(t, strings.contains(llvm_text, "declare i32 @used(i32, i32)")) testing.expect(t, strings.contains(llvm_text, "call i32 @used(i32 1, i32 2)")) testing.expect(t, !strings.contains(llvm_text, "@unused(")) testing.expect(t, strings.contains(llvm_text, "define i32 @bro_c__p0__bodyful__i32")) } @(test) c_primitives_remain_distinct_with_apple_silicon_representations :: proc(t: ^testing.T) { testing.expect(t, types.C_CHAR != types.C_SCHAR) testing.expect(t, types.C_SCHAR != types.C_UCHAR) testing.expect(t, types.C_INT != types.I32) testing.expect(t, types.C_ULONG != types.U64) testing.expect_value(t, types.representation(types.C_CHAR), types.I8) testing.expect_value(t, types.representation(types.C_SCHAR), types.I8) testing.expect_value(t, types.representation(types.C_UCHAR), types.U8) testing.expect_value(t, types.representation(types.C_SHORT), types.I16) testing.expect_value(t, types.representation(types.C_USHORT), types.U16) testing.expect_value(t, types.representation(types.C_INT), types.I32) testing.expect_value(t, types.representation(types.C_UINT), types.U32) testing.expect_value(t, types.representation(types.C_LONG), types.I64) testing.expect_value(t, types.representation(types.C_ULONG), types.U64) testing.expect_value(t, types.representation(types.C_LONGLONG), types.I64) testing.expect_value(t, types.representation(types.C_ULONGLONG), types.U64) testing.expect_value(t, types.representation(types.C_FLOAT), types.F32) testing.expect_value(t, types.representation(types.C_DOUBLE), types.F64) testing.expect_value(t, types.representation(types.C_LONGDOUBLE), types.F64) testing.expect_value(t, types.c_vararg_promotion(types.I8), types.C_INT) testing.expect_value(t, types.c_vararg_promotion(types.U16), types.C_INT) testing.expect_value(t, types.c_vararg_promotion(types.C_CHAR), types.C_INT) testing.expect_value(t, types.c_vararg_promotion(types.C_USHORT), types.C_INT) testing.expect_value(t, types.c_vararg_promotion(types.F32), types.C_DOUBLE) testing.expect_value(t, types.c_vararg_promotion(types.C_FLOAT), types.C_DOUBLE) testing.expect_value(t, types.c_vararg_promotion(types.U32), types.U32) testing.expect_value(t, types.c_vararg_promotion(types.C_DOUBLE), types.C_DOUBLE) testing.expect_value(t, target.llvm_triple(target.DEFAULT), "arm64-apple-macosx13.0.0") } @(test) interop_foundation_emits_compounds_and_narrow_c_abi_attributes :: proc(t: ^testing.T) { text := `Point :: struct { x i32 y i32 } signed c_func(value c_char) c_char unsigned c_func(value c_uchar) c_uchar exact c_func(value u32) u32 fallback func() i32 { return 9 } main func() void { c :: 1 values [2;0]mut u8 = [1, 2] point Point :: Point{x = 3, y = 4} maybe ?i32 = 5 _ = c _ = values[2] _ = (&values).ptr + 1 _ = values.len _ = values[0..2] _ = "hello".ptr _ = "hello".len _ = point.x _ = maybe? _ = maybe orelse 0 _ = maybe orelse fallback() _ = signed(1) _ = unsigned(1) _ = exact(1) } ` 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) testing.expect(t, strings.contains(llvm_text, "target triple = \"arm64-apple-macosx13.0.0\"")) testing.expect(t, strings.contains(llvm_text, "declare signext i8 @signed(i8 signext)")) testing.expect(t, strings.contains(llvm_text, "declare zeroext i8 @unsigned(i8 zeroext)")) testing.expect(t, strings.contains(llvm_text, "declare i32 @exact(i32)")) testing.expect(t, strings.contains(llvm_text, "@bro.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\"")) testing.expect(t, strings.contains(llvm_text, "getelementptr [3 x i8]")) testing.expect(t, strings.contains(llvm_text, "attempted to unwrap none")) testing.expect(t, strings.contains(llvm_text, "orelse_fallback")) testing.expect(t, strings.contains(llvm_text, "orelse_some")) } @(test) string_literals_preserve_static_length_and_sentinel_through_pointer_views :: proc(t: ^testing.T) { text := `take_sentinel_pointer func(value [*;0]u8) void {} take_mut_sentinel_pointer func(value [*;0]mut u8) void {} take_pointer func(value *u8) void {} take_sentinel_slice func(value [;0]u8) void {} take_mut_sentinel_slice func(value [;0]mut u8) void {} take_slice func(value []u8) void {} take_c_string c_func(value *c_char) c_int take_c_sentinel c_func(value [*;0]c_char) c_int main func() void { text :: "hello" values [2;0]mut u8 = [1, 2] pointer :: &values _ = text.len _ = text.ptr _ = text[0] _ = text[1..] _ = pointer.len _ = pointer.ptr _ = pointer[0] _ = pointer[1..] offset [*;0]u8 :: text.ptr + 1 suffix [*;0]u8 :: text[1..].ptr middle []u8 :: text[1..3] _ = offset _ = suffix _ = middle take_sentinel_pointer(text) take_pointer(text) take_sentinel_slice(text) take_slice(text) take_mut_sentinel_pointer(pointer) take_mut_sentinel_slice(pointer) take_sentinel_pointer(pointer) take_sentinel_slice(pointer) _ = take_c_string(text) _ = take_c_sentinel(text) _ = take_c_string(text.ptr) _ = take_c_sentinel(text.ptr) } ` 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) string_type := types.INVALID decays := 0 for expr in hir_module.exprs { if expr.kind == .String { string_type = expr.type } if expr.kind == .Decay_Array_Pointer { decays += 1 } } pointer, array, string_ok := types.array_pointer(string_type, &hir_module.types) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, string_ok && !pointer.mutable && array.child == types.U8 && array.count == 5 && array.has_sentinel && array.sentinel == 0) testing.expect(t, decays >= 6) testing.expect(t, strings.contains(llvm_text, "@bro.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\"")) testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_string(ptr)")) testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_sentinel(ptr)")) } @(test) multiline_strings_join_lines_and_strip_indentation :: proc(t: ^testing.T) { // Source written double-quoted (not a raw `...` literal) because the // backtick is the multi-line string marker. Covers basic join, the // trailing-newline form, a blank line in the middle, and value-on-next-line. text := "main func() void {\n" + "\tbasic ::\n\t\t`a\n\t\t`b\n" + "\ttrailing ::\n\t\t`hello\n\t\t`world\n\t\t`\n" + "\tgapped ::\n\t\t`x\n\t\t`\n\t\t`y\n" + "\t_ = basic\n\t_ = trailing\n\t_ = gapped\n}\n" 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(ast_module.strings), 3) testing.expect_value(t, ast_module.strings[0], "a\nb") testing.expect_value(t, ast_module.strings[1], "hello\nworld\n") testing.expect_value(t, ast_module.strings[2], "x\n\ny") // A multi-line string is an ordinary string literal: @[N;0]u8. string_type := types.INVALID for expr in hir_module.exprs { if expr.kind == .String { string_type = expr.type break } } pointer, array, string_ok := types.array_pointer(string_type, &hir_module.types) testing.expect(t, string_ok && !pointer.mutable && array.child == types.U8 && array.has_sentinel && array.sentinel == 0) } @(test) array_pointer_and_c_string_coercion_restrictions_are_diagnosed :: proc(t: ^testing.T) { text := `take_c_string c_func(value *c_char) c_int take_mut_c_string c_func(value *mut c_char) c_int take_pointer func(value *u8) void {} take_mut_pointer func(value *mut u8) void {} take_slice func(value []u8) void {} bad_sentinel func(value [*;256]u8) void {} main func() void { values [1;0]mut u8 = [1] _ = values.ptr take_pointer(values) take_slice(values) ordinary *u8 :: "hello" nonzero [1;'\n']mut u8 = [1] take_pointer("hello"[1..]) _ = take_c_string(ordinary) _ = take_c_string((&nonzero).ptr) _ = take_c_string(1) take_mut_pointer("hello") _ = take_mut_c_string("hello") } ` 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) conversion_errors := 0 array_ptr_error := false sentinel_error := false for diagnostic in diagnostics.items { conversion_errors += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0 array_ptr_error = array_ptr_error || strings.contains(diagnostic.message, "arrays do not expose '.ptr'") sentinel_error = sentinel_error || strings.contains(diagnostic.message, "sentinel value does not fit array, slice, or pointer") } testing.expect_value(t, conversion_errors, 8) testing.expect(t, array_ptr_error) testing.expect(t, sentinel_error) } @(test) immutable_pointer_and_slice_bindings_preserve_mutable_pointees :: proc(t: ^testing.T) { text := `main func() void { values [2]mut u8 = [1, 2] pointer *mut u8 :: (&values).ptr slice []mut u8 :: values[0..] pointer[0] = 3 slice[1] = 4 } ` 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) testing.expect_value(t, len(diagnostics.items), 0) } @(test) slicing_an_array_variable_takes_its_address_implicitly :: proc(t: ^testing.T) { // Milestone 10: `arr[a..b]` on an array variable slices without an explicit // `&`. The slice operand must be a pointer to the array (getelementptr off a // `ptr`), not the array value. text := `sink func(s []i32) i32 { return s[0] } main func() i32 { arr [4]i32 = [10, 20, 30, 40] full :: sink(arr[..]) part :: sink(arr[1..3]) return full + part } ` 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) testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr")) } @(test) slicing_an_array_rvalue_materializes_a_temporary :: proc(t: ^testing.T) { // The checker accepts slicing a non-location array (here, a by-value array // return). Lowering must store it into a temporary and slice that address; // otherwise the slice operand is an array value, which is an invalid pointer. text := `make_arr func() [4]i32 { return [1, 2, 3, 4] } sink func(s []i32) i32 { return s[0] } main func() i32 { return sink(make_arr()[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) // The materialization store of the array rvalue into its temporary. testing.expect(t, strings.contains(llvm_text, "store [4 x i32]")) testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr")) } @(test) field_and_index_access_on_an_rvalue_aggregate_materializes_it :: proc(t: ^testing.T) { // A by-value struct return is a temporary with no address. Reading a field, // slicing an array field, and taking its address must spill it into a // temporary and address that, rather than addressing the aggregate value. text := `Box :: struct { score i32 data [4]i32 } make_box func() Box { return Box { score = 7, data = [1, 2, 3, 4] } } sink func(s []i32) i32 { return s[0] } main func() i32 { s :: make_box().score v :: sink(make_box().data[0..]) p :: &make_box().data return s + v + p^[1] } ` 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) // The rvalue Box is spilled into a stack temporary (structs render as // %bro.type.N), then stored, then its fields are addressed off a `ptr`. // A regression addresses the aggregate value directly, which llc rejects. testing.expect(t, strings.contains(llvm_text, "alloca %bro.type.")) testing.expect(t, strings.contains(llvm_text, "store %bro.type.")) testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr")) } @(test) c_variadic_calls_promote_extras_and_emit_variadic_llvm :: proc(t: ^testing.T) { text := `variadic c_func(tag c_int, ...) c_int zero c_func(...) void main func() void { narrow i8 :: -2 unsigned u16 :: 3 float_value f32 :: 4.0 c_float_value c_float :: 5.0 pointer *u8 :: "ok".ptr nullable ?*u8 :: pointer zero(pointer) _ = variadic(7, narrow, unsigned, float_value, c_float_value, pointer, nullable) } ` 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) promotions := 0 for expr in hir_module.exprs { if expr.kind == .C_Vararg_Promote { promotions += 1 } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, promotions, 4) found_hir_variadic := false for function in hir_module.functions { found_hir_variadic = found_hir_variadic || function.variadic } found_ir_variadic := false for function in ir_module.functions { found_ir_variadic = found_ir_variadic || function.variadic } testing.expect(t, found_hir_variadic) testing.expect(t, found_ir_variadic) testing.expect(t, strings.contains(llvm_text, "declare i32 @variadic(i32, ...)")) testing.expect(t, strings.contains(llvm_text, "declare void @zero(...)")) testing.expect(t, strings.contains(llvm_text, "sext i8")) testing.expect(t, strings.contains(llvm_text, "zext i16")) testing.expect(t, strings.contains(llvm_text, "fpext float")) testing.expect(t, strings.contains(llvm_text, "call void (...) @zero(ptr")) testing.expect(t, strings.contains(llvm_text, "call i32 (i32, ...) @variadic(i32 7, i32")) testing.expect(t, strings.contains(llvm_text, "double")) testing.expect(t, strings.contains(llvm_text, "ptr")) } @(test) c_variadic_restrictions_and_extra_argument_types_are_diagnosed :: proc(t: ^testing.T) { text := `Record :: c_struct { value c_int } foreign c_func(...) void requires c_func(value c_int, ...) void native func(...) void bodyful c_func(...) void {} main func() void { values [1]u8 :: [1] record Record :: Record { value = 1 } foreign(values) foreign(record) requires() } ` 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) restricted := 0 found_extra := false found_arity := false for diagnostic in diagnostics.items { if strings.contains(diagnostic.message, "must be a bodyless 'c_func' declaration") { restricted += 1 } found_extra = found_extra || strings.contains(diagnostic.message, "C variadic argument must be a concrete scalar or pointer") found_arity = found_arity || strings.contains(diagnostic.message, "expects at least 1 arguments") } testing.expect_value(t, restricted, 2) testing.expect(t, found_extra) testing.expect(t, found_arity) } @(test) variadicness_is_part_of_c_function_signature_compatibility :: proc(t: ^testing.T) { fixed := ast.Function{result=types.C_INT} variadic := ast.Function{result=types.C_INT, variadic=true} testing.expect(t, !checker.function_signatures_equal(fixed, variadic)) testing.expect(t, !loader.function_signatures_equal(fixed, nil, types.C_INT, true)) } @(test) c_structs_are_by_value_and_may_be_opaque :: proc(t: ^testing.T) { text := `Defined :: c_struct { value c_int } Opaque :: c_struct Empty :: c_struct {} Bad :: c_struct { values []i32 } read c_func(value @Defined) c_int pass c_func(value Defined) Defined bad_opaque c_func(value Opaque) void main func() void { _ = pass(Defined { value = 1 }) } ` 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) found_opaque := false found_bad_layout := false found_empty := false for diagnostic in diagnostics.items { found_opaque = found_opaque || strings.contains(diagnostic.message, "cannot be passed by value") found_bad_layout = found_bad_layout || strings.contains(diagnostic.message, "C-layout-compatible") found_empty = found_empty || strings.contains(diagnostic.message, "at least one field") } testing.expect(t, found_opaque) testing.expect(t, found_bad_layout) testing.expect(t, found_empty) } @(test) aarch64_c_record_abi_classifies_fixed_parameters_and_results :: proc(t: ^testing.T) { text := `Small :: c_struct { left c_int right c_int } Medium :: c_struct { first c_int second c_int third c_int } Hfa :: c_struct { x c_float y c_float } Large :: c_struct { first c_long second c_long third c_long } small c_func(value Small) Small medium c_func(value Medium) Medium hfa c_func(value Hfa) Hfa large c_func(value Large) Large main func() void { _ = small(Small { left = 1, right = 2 }) _ = medium(Medium { first = 1, second = 2, third = 3 }) _ = hfa(Hfa { x = 1.0, y = 2.0 }) _ = large(Large { first = 1, second = 2, third = 3 }) } ` 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) testing.expect(t, strings.contains(llvm_text, "declare i64 @small(i64)")) testing.expect(t, strings.contains(llvm_text, "declare [2 x i64] @medium([2 x i64])")) testing.expect(t, strings.contains(llvm_text, "@hfa([2 x float])")) testing.expect(t, strings.contains(llvm_text, "declare void @large(ptr sret(")) testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64")) } @(test) invalid_foreign_declarations_are_eagerly_diagnosed_and_calls_trap :: proc(t: ^testing.T) { text := `bad c_func(value int) int native func() i32 main func() void { _ = bad(1) } ` 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) found_parameter := false found_result := false found_native := false for diagnostic in diagnostics.items { found_parameter = found_parameter || strings.contains(diagnostic.message, "requires concrete parameter types") found_result = found_result || strings.contains(diagnostic.message, "requires a concrete or void result type") found_native = found_native || strings.contains(diagnostic.message, "must use 'c_func'") } testing.expect(t, found_parameter) testing.expect(t, found_result) testing.expect(t, found_native) testing.expect(t, !strings.contains(llvm_text, "@bad(")) testing.expect(t, strings.contains(llvm_text, "call void @bro.trap")) } @(test) duplicate_foreign_symbols_across_packages_are_poisoned :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) ast_module, loaded := loader.load("examples/packages/foreign_duplicate/app", &sources, &diagnostics, &symbols) 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) duplicate_count := 0 for diagnostic in diagnostics.items { if strings.contains(diagnostic.message, "duplicate foreign symbol 'same'") { duplicate_count += 1 } } testing.expect(t, loaded) testing.expect_value(t, duplicate_count, 2) testing.expect(t, !strings.contains(llvm_text, "@same(")) testing.expect(t, strings.contains(llvm_text, "call void @bro.trap")) } @(test) bodyless_root_main_recovers_as_a_trap_definition :: proc(t: ^testing.T) { text := "main c_func() i32\n" 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "main must have a body") } testing.expect(t, found) testing.expect_value(t, len(ir_module.functions), 1) testing.expect_value(t, ir_module.functions[0].implementation, ir.Implementation.Definition) testing.expect(t, strings.contains(llvm_text, "define i32 @main()")) testing.expect(t, !strings.contains(llvm_text, "declare i32 @main()")) } @(test) literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) { text := `return_i16 func() i16 { return 1 + 2 } take_i16 func(value i16) i16 { return value } take_int func(value int) int { return value } main func() void { local i16 :: 1 + 2 _ = 100 + (20 + 8) _ = return_i16() _ = take_i16(1 + 2) _ = take_int(127 + 1) } ` 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) testing.expect(t, strings.contains(llvm_text, "@bro__p0__take_int__i16")) for function in ir_module.functions { for instruction in function.instructions { testing.expect(t, instruction.op != ir.Opcode.Add_Checked) } } for function in hir_module.functions { for statement_id in function.body { statement := hir_module.statements[statement_id] if statement.expr < 0 { continue } expr := hir_module.exprs[statement.expr] if expr.kind == .Integer { testing.expect(t, types.equal(expr.type, types.I16)) } if expr.kind == .Call && symbol.resolve(&symbols, function.name) == "main" && len(expr.args) > 0 { arg := hir_module.exprs[expr.args[0]] testing.expect_value(t, arg.kind, hir.Expr_Kind.Integer) testing.expect(t, types.equal(arg.type, types.I16)) } } } } @(test) runtime_arithmetic_does_not_inherit_result_context :: proc(t: ^testing.T) { text := `widen_after_add func(value i8) i16 { return value + 1 } main func() void { _ = widen_after_add(1) } ` 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) testing.expect_value(t, len(diagnostics.items), 0) found := false for function in hir_module.functions { if symbol.resolve(&symbols, function.name) != "widen_after_add" { continue } found = true statement := hir_module.statements[function.body[0]] widen := hir_module.exprs[statement.expr] add := hir_module.exprs[widen.left] testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen) testing.expect(t, types.equal(widen.type, types.I16)) testing.expect_value(t, add.kind, hir.Expr_Kind.Add) testing.expect(t, types.equal(add.type, types.I8)) } testing.expect(t, found) } @(test) parser_recovers_after_invalid_tokens :: proc(t: ^testing.T) { text := `broken @ declaration main func() void {} ` 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) testing.expect(t, len(diagnostics.items) > 0) testing.expect_value(t, len(module.functions), 1) testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "main") } @(test) return_sink_and_unconsumed_values_have_distinct_hir :: proc(t: ^testing.T) { text := `give func() i8 { return 1 } done func() void { return _ } main func() void { done() _ = give() give() } ` 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) done_id, main_id := -1, -1 for function, id in hir_module.functions { if symbol.resolve(&symbols, function.name) == "done" { done_id = id } else if symbol.resolve(&symbols, function.name) == "main" { main_id = id } } testing.expect(t, done_id >= 0) testing.expect(t, main_id >= 0) testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].kind, hir.Stmt_Kind.Return) testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].expr, hir.INVALID_EXPR) main := hir_module.functions[main_id] testing.expect_value(t, hir_module.statements[main.body[0]].kind, hir.Stmt_Kind.Expression) testing.expect_value(t, hir_module.statements[main.body[1]].kind, hir.Stmt_Kind.Sink) testing.expect_value(t, hir_module.statements[main.body[2]].kind, hir.Stmt_Kind.Trap) } @(test) recursive_specialization_reaches_a_fixed_point :: proc(t: ^testing.T) { text := `a func(value int) i32 { return b(value) } b func(value int) i32 { return a(value) } main func() void { _ = a(1) } ` 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(hir_module.functions), 3) } @(test) stale_specializations_are_pruned_after_inference :: proc(t: ^testing.T) { text := `derived :: identity(make()) wide :: delayed() identity func(value int) int { return value } make func() int { return wide return 1 } delayed func() int { return 128 } main func() void {} ` 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) testing.expect_value(t, len(hir_module.functions), 4) testing.expect(t, strings.contains(llvm_text, "@bro__p0__identity__i16")) testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity__i8")) } @(test) eager_global_calls_root_specializations :: proc(t: ^testing.T) { text := `make func() i32 { return 7 } unused_native func() i32 { return 9 } unused_foreign c_func() i32 value i32 :: make() main func() void {} ` 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) testing.expect_value(t, len(hir_module.functions), 2) testing.expect(t, strings.contains(llvm_text, "@bro__p0__make(")) testing.expect(t, !strings.contains(llvm_text, "@bro__p0__unused_native(")) testing.expect(t, !strings.contains(llvm_text, "@unused_foreign(")) } @(test) malformed_generic_calls_do_not_retain_specializations :: proc(t: ^testing.T) { text := `identity func(value int) int { return value } bad :: identity(1, 2) main func() void {} ` 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) found_arity := false for diagnostic in diagnostics.items { found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 arguments, got 2") } testing.expect(t, found_arity) testing.expect_value(t, len(hir_module.functions), 1) testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity")) testing.expect(t, strings.contains(llvm_text, "call void @bro.trap")) } @(test) long_generic_call_chain_reaches_a_fixed_point :: proc(t: ^testing.T) { builder := strings.builder_make() defer strings.builder_destroy(&builder) for index in 0 ..< 70 { fmt.sbprintf(&builder, "fn%d func(value int) int ", index) strings.write_string(&builder, "{ return ") if index == 69 { strings.write_string(&builder, "value") } else { fmt.sbprintf(&builder, "fn%d(value)", index+1) } strings.write_string(&builder, " }\n") } strings.write_string(&builder, "main func() i32 { return fn0(1) }\n") source_file := source.Source{path="test.bro", text=strings.to_string(builder)} 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(hir_module.functions), 71) } @(test) globals_and_generic_results_reach_a_shared_fixed_point :: proc(t: ^testing.T) { text := `derived :: identity(base) base :: make() identity func(value int) int { return value } make func() int { return 1 } main func() i32 { return derived } ` 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(hir_module.functions), 3) for global in hir_module.globals { testing.expect(t, types.equal(global.type, types.I8)) } } @(test) unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) { text := `broken func(value, value i8, nope void) void {} main func() void {} ` 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) found_duplicate := false found_void := false for diagnostic in diagnostics.items { found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate parameter 'value'") found_void = found_void || strings.contains(diagnostic.message, "void is only valid as a function result type") } testing.expect(t, found_duplicate) testing.expect(t, found_void) } run_command_success :: proc(command: []string) -> bool { state, stdout, stderr, err := os2.process_exec( os2.Process_Desc{command=command}, context.allocator, ) delete(stdout) delete(stderr) return err == nil && state.exit_code == 0 } prepare_native_object :: proc(output: string) -> bool { local_cache := fmt.aprintf("ZIG_LOCAL_CACHE_DIR=%s-zig-cache", output) defer delete(local_cache) global_cache := fmt.aprintf("ZIG_GLOBAL_CACHE_DIR=%s-zig-global-cache", output) defer delete(global_cache) return run_command_success([]string{ "/usr/bin/env", local_cache, global_cache, "zig", "cc", "-c", "examples/interop/manual/native.c", "-o", output, }) } prepare_native_archive :: proc(object, archive: string) -> bool { return run_command_success([]string{ "/usr/bin/ar", "-rcs", archive, object, }) } run_executable :: proc(path: string) -> os2.Process_State { state, stdout, stderr, _ := os2.process_exec( os2.Process_Desc{command=[]string{path}}, context.allocator, ) delete(stdout) delete(stderr) return state } @(test) valid_program_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-valid" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/prototype", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) c_printf_accepts_a_string_literal :: proc(t: ^testing.T) { output := "/tmp/brolang-test-printf" defer _ = os.remove(output) status := compiler_core.compile_package("examples/interop/printf", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) sentinel_pointer_views_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-sentinel-pointer" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/sentinel_pointer", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 303) } @(test) control_flow_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-control-flow" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/control_flow", output) testing.expect_value(t, status, 0) state, stdout, stderr, _ := os2.process_exec( os2.Process_Desc{command=[]string{output}}, context.allocator, ) defer delete(stdout) defer delete(stderr) // if / else if / else, comparisons, logical and/or/not, bool locals, and // block scoping together produce 42. testing.expect_value(t, state.exit_code, 42) // Short-circuit: `noisy()` is never reached, so its output must be absent, // while the taken or-branch must print. testing.expect(t, !strings.contains(string(stdout), "rhs-evaluated")) testing.expect(t, strings.contains(string(stdout), "or-taken")) } @(test) break_and_continue_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-break-continue" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/break_continue", output) testing.expect_value(t, status, 0) state := run_executable(output) // `while` break, range-for `continue`, nested innermost-targeting break, a // `continue` on the final element of an inclusive `u8` range (no overflow // trap), and an exitable `while true` together produce 42. testing.expect_value(t, state.exit_code, 42) } @(test) break_and_continue_misuse_is_diagnosed :: proc(t: ^testing.T) { // `break`/`continue` outside any loop are rejected, and a non-void function // that exits a `while true` via `break` without returning is flagged as // missing a return (the `all_paths_return` refinement). text := `main func() i32 { bad_break() bad_continue() return missing_return() } bad_break func() void { break } bad_continue func() void { continue } missing_return func() i32 { while true { break } } ` 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) break_outside := false continue_outside := false missing := false for diagnostic in diagnostics.items { break_outside = break_outside || strings.contains(diagnostic.message, "'break' outside of a loop") continue_outside = continue_outside || strings.contains(diagnostic.message, "'continue' outside of a loop") missing = missing || strings.contains(diagnostic.message, "'missing_return' does not return a value") } testing.expect(t, break_outside) testing.expect(t, continue_outside) testing.expect(t, missing) } @(test) defer_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-defer" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/defer", output) testing.expect_value(t, status, 0) state := run_executable(output) // LIFO, runs on fall-through / break / continue / return (with the return value // captured before defers run), scoped bare blocks, and `defer { ... }` blocks // together produce 42. testing.expect_value(t, state.exit_code, 42) } @(test) defer_misuse_is_diagnosed :: proc(t: ^testing.T) { // Deferring control flow that would escape the defer is rejected: `defer return`, // `defer break`, and a `return` inside a `defer { ... }` block. text := `main func() i32 { bad_defer_return() bad_defer_break() bad_return_in_defer() return 0 } bad_defer_return func() void { defer return } bad_defer_break func() void { for 0..3 |i| { defer break _ = i } } bad_return_in_defer func() void { defer { return } } ` 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) defer_return := false defer_break := false return_in_defer := false for diagnostic in diagnostics.items { defer_return = defer_return || strings.contains(diagnostic.message, "cannot defer a 'return' statement") defer_break = defer_break || strings.contains(diagnostic.message, "cannot defer a 'break' statement") return_in_defer = return_in_defer || strings.contains(diagnostic.message, "cannot 'return' inside a 'defer'") } testing.expect(t, defer_return) testing.expect(t, defer_break) testing.expect(t, return_in_defer) } @(test) yield_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-yield" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/yield", output) testing.expect_value(t, status, 0) state := run_executable(output) // Value blocks, value if-statements (incl. `return` branches and unwrap-`if`), // `orelse`, value loops, labeled value blocks (`blk: { … yield :blk v }`, incl. // defer-capture, `{T,none}` → optional, and `none` before a concrete yield that uses a // block local), outer-loop control (`yield :outer v` / `break :outer`), and labeled // block statements exited via `break :blk` together produce 42. testing.expect_value(t, state.exit_code, 42) } @(test) native_union_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-unions" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/unions", output) testing.expect_value(t, status, 0) state := run_executable(output) // A native untagged union (`Val :: union { n i32, f f64 }`) constructed via a keyed // literal, stored into a local, and read back through field access reinterprets the // carrier and yields 42 (declaration → construct → store → load → field read). testing.expect_value(t, state.exit_code, 42) } @(test) tagged_union_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-tagged-union" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/tagged_union", output) testing.expect_value(t, status, 0) state := run_executable(output) // Both tagged forms — `union(Animal)` (existing enum tag) and `union(enum)` (synthesized // tag) — constructed via keyed literals, stored as `{tag, payload}`, with the active // payload read back at its post-tag offset: 37 + 5 = 42. Non-zero payloads make a wrong // payload offset (e.g. overlapping the tag) fail the exit code. testing.expect_value(t, state.exit_code, 42) } @(test) tagged_union_stores_the_discriminant :: proc(t: ^testing.T) { // The runtime test observes only the payload; this one checks the *tag* is written. // Native sum tags are global u16 IDs. `Animal` contributes dog/cat/bird (1..3), // then `Data` contributes dog:i32/bird:i32 (4..5), so `Data{ bird = 99 }` // writes discriminant `store i16 5` beside the payload. text := `Animal :: enum { dog cat bird } Data :: union(Animal) { dog i32 bird i32 } main func() i32 { x Data = Data{ bird = 99 } return x.bird } ` 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) testing.expect(t, strings.contains(llvm_text, "{ i16, [2 x i8], i32 }")) testing.expect(t, strings.contains(llvm_text, "store i16 5,")) testing.expect(t, strings.contains(llvm_text, "store i32 99,")) } @(test) tagged_union_validation_is_diagnosed :: proc(t: ^testing.T) { // A `union(T)` tag must be an enum, and every variant of a `union(Enum)` must name a // member of that enum. text := `Color :: struct { r u8 } Animal :: enum { dog cat } BadTag :: union(Color) { dog i32 } BadVariant :: union(Animal) { snake i32 } main func() i32 { 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) found_tag := false found_variant := false for diagnostic in diagnostics.items { found_tag = found_tag || strings.contains(diagnostic.message, "tagged union's tag must be an enum") found_variant = found_variant || strings.contains(diagnostic.message, "'snake' is not a member of the tag enum") } testing.expect(t, found_tag) testing.expect(t, found_variant) } @(test) sum_composition_merges_widens_and_rejects_conflicts :: proc(t: ^testing.T) { text := `A :: enum { same left } B :: enum { same right } Both :: alias A | B UA :: union(enum) { item i32 } UB :: union(enum) { empty void } UBoth :: alias UA | UB pick func(value Both) i32 { match value { .same: return 1 .left: return 2 .right: return 3 } } payload func(value UBoth) i32 { match value { .item |n|: return n .empty: return 5 } } main func() i32 { a A = .left b B = .right u UA = UA{ item = 4 } v UB = .empty return pick(.same) + pick(a) + pick(b) + payload(u) + payload(v) } ` 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) testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64")) conflict := `A :: union(enum) { dup i32 } B :: union(enum) { dup i64 } Bad :: alias A | B main func() void {} ` conflict_source := source.Source{path="conflict.bro", text=conflict} conflict_diagnostics := source.init_diagnostics(&conflict_source) defer source.destroy_diagnostics(&conflict_diagnostics) conflict_symbols := symbol.init_table() defer symbol.destroy_table(&conflict_symbols) conflict_stream := lexer.lex(&conflict_source, &conflict_diagnostics, &conflict_symbols) defer delete(conflict_stream.items) conflict_module := parser.parse(&conflict_stream, &conflict_source, &conflict_diagnostics) defer ast.destroy_module(&conflict_module) found_conflict := false for diagnostic in conflict_diagnostics.items { found_conflict = found_conflict || strings.contains(diagnostic.message, "same variant name") } testing.expect(t, found_conflict) backed := `A :: enum(u8) { a = 1 } B :: enum { b } Bad :: alias A | B main func() void {} ` backed_source := source.Source{path="backed.bro", text=backed} backed_diagnostics := source.init_diagnostics(&backed_source) defer source.destroy_diagnostics(&backed_diagnostics) backed_symbols := symbol.init_table() defer symbol.destroy_table(&backed_symbols) backed_stream := lexer.lex(&backed_source, &backed_diagnostics, &backed_symbols) defer delete(backed_stream.items) backed_module := parser.parse(&backed_stream, &backed_source, &backed_diagnostics) defer ast.destroy_module(&backed_module) found_backed := false for diagnostic in backed_diagnostics.items { found_backed = found_backed || strings.contains(diagnostic.message, "native unbacked") } testing.expect(t, found_backed) } @(test) yield_inference_visits_yielded_calls :: proc(t: ^testing.T) { text := `identity func(value int) int { return value } main func() i32 { value :: { yield identity(41) } return value - 41 } ` source_file := source.Source{path="yield_call.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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, len(hir_module.functions) > 1) } @(test) errors_example_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-errors" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/errors", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) match_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-match" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/match", output) testing.expect_value(t, status, 0) state := run_executable(output) // Exercises every match form — tagged-union statement match with payload capture, // tagged-union value match (implicit + explicit yield), enum statement/value match, // and integer match with `else` — all summed to a self-checking 0 on success. testing.expect_value(t, state.exit_code, 0) } @(test) match_dispatches_on_the_tag :: proc(t: ^testing.T) { // A tagged-union match desugars to: read the discriminant once (a load of the tag // enum at the union's offset 0), then compare it against each variant's tag value. // Data's runtime tag is the hidden global u16 variant ID. text := `Animal :: enum { dog cat bird } Data :: union(Animal) { dog i32 bird i32 } main func() i32 { d Data = Data{ bird = 7 } out i32 = 0 match d { .dog |v|: out = v .bird |v|: out = v + 1 } return out } ` 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) testing.expect(t, strings.contains(llvm_text, "load i16")) testing.expect(t, strings.contains(llvm_text, "icmp eq i16")) } @(test) match_misuse_is_diagnosed :: proc(t: ^testing.T) { // Five rejected matches: (1) a non-exhaustive enum match with no `else`, (2) a scalar // match missing its mandatory `else`, (3) a payload capture on a non-union arm, (4) a // redundant `else` on an already-exhaustive match, and (5) an unknown variant. text := `Animal :: enum { dog cat bird } Data :: union(Animal) { dog i32 bird i32 } not_exhaustive func(a Animal) i32 { match a { .dog: { return 1 } .cat: { return 2 } } return 0 } missing_else func(n i32) i32 { match n { 0: { return 1 } 1: { return 2 } } return 0 } bad_capture func(a Animal) i32 { match a { .dog |v|: { return 1 } .cat: { return 2 } .bird: { return 3 } } return 0 } redundant_else func(a Animal) i32 { match a { .dog: { return 1 } .cat: { return 2 } .bird: { return 3 } else: { return 4 } } return 0 } unknown_variant func(d Data) i32 { match d { .dog: { return 1 } .snake: { return 2 } else: { return 3 } } return 0 } main func() i32 { # Functions are specialized on use, so call each so its body is type-checked. d Data = Data{ dog = 0 } return not_exhaustive(.dog) + missing_else(0) + bad_capture(.dog) + redundant_else(.dog) + unknown_variant(d) } ` 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) found_exhaustive := false found_missing_else := false found_capture := false found_redundant := false found_unknown := false for diagnostic in diagnostics.items { found_exhaustive = found_exhaustive || strings.contains(diagnostic.message, "is not exhaustive") found_missing_else = found_missing_else || strings.contains(diagnostic.message, "requires an 'else' arm") found_capture = found_capture || strings.contains(diagnostic.message, "only tagged-union variants can capture") found_redundant = found_redundant || strings.contains(diagnostic.message, "redundant 'else'") found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown variant '.snake'") } testing.expect(t, found_exhaustive) testing.expect(t, found_missing_else) testing.expect(t, found_capture) testing.expect(t, found_redundant) testing.expect(t, found_unknown) } @(test) match_range_arm_emits_bounds :: proc(t: ^testing.T) { // A scalar range arm `lo..hi:` desugars to `key >= lo and key < hi` (inclusive uses // `<=`), emitted as signed integer comparisons for an i32 subject. text := `main func() i32 { n i32 = 5 out i32 = 0 match n { 0..10: out = 1 10..=20: out = 2 else: out = 3 } return out } ` 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) testing.expect(t, strings.contains(llvm_text, "icmp sge i32")) // key >= lo testing.expect(t, strings.contains(llvm_text, "icmp slt i32")) // key < hi (exclusive) testing.expect(t, strings.contains(llvm_text, "icmp sle i32")) // key <= hi (inclusive) } @(test) match_extended_misuse_is_diagnosed :: proc(t: ^testing.T) { // Five rejected forms from milestone 22.5: (1) a capture on a void variant, (2) a value // given to a void variant in construction, (3) a bare key on a non-void field, (4) a // range pattern on an enum subject, and (5) a multi-pattern capture whose variants have // different payload types. (Pointer capture on an rvalue subject is also rejected, but // match-on-call-result is a separate pre-existing gap so it isn't exercised here.) text := `Animal :: enum { dog cat bird } Point :: struct { x i32 y i32 } Box :: union(enum) { point Point count i32 empty void } void_capture func(b Box) i32 { match b { .point |p|: { return p.x } .count |c|: { return c } .empty |x|: { return 0 } } return 0 } void_value func() i32 { b Box = Box{ empty = 5 } return 0 } bare_on_nonvoid func() i32 { b Box = Box{ count } return 0 } range_on_enum func(a Animal) i32 { match a { 0..2: { return 1 } else: { return 0 } } return 0 } incompatible_capture func(b Box) i32 { match b { .point, .count |v|: { return 0 } .empty: { return 0 } } return 0 } main func() i32 { b Box = Box{ count = 1 } return void_capture(b) + void_value() + bare_on_nonvoid() + range_on_enum(.dog) + incompatible_capture(b) } ` 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) found_void_capture := false found_void_value := false found_bare := false found_range_enum := false found_incompatible := false for diagnostic in diagnostics.items { found_void_capture = found_void_capture || strings.contains(diagnostic.message, "void payload") found_void_value = found_void_value || strings.contains(diagnostic.message, "void variant 'empty' takes no value") found_bare = found_bare || strings.contains(diagnostic.message, "field 'count' requires a value") found_range_enum = found_range_enum || strings.contains(diagnostic.message, "range patterns only apply to scalar") found_incompatible = found_incompatible || strings.contains(diagnostic.message, "capture group with incompatible types") } testing.expect(t, found_void_capture) testing.expect(t, found_void_value) testing.expect(t, found_bare) testing.expect(t, found_range_enum) testing.expect(t, found_incompatible) } @(test) match_call_subject_and_contextual_void_compile :: proc(t: ^testing.T) { // Milestone 22.6: (1) a call expression directly as the match subject (`match get()`) // now specializes — previously "could not resolve specialization of 'get'" — because the // inference pass visits the match subject; (2) a void variant constructed contextually // (`e Box = .empty`) coerces the bare enum literal to the union. Both compile clean to IR. text := `Animal :: enum { dog cat bird } Box :: union(enum) { count i32 empty void } get func() Animal { return .bird } main func() i32 { e Box = .empty r i32 = 0 match get() { .dog: r = 1 .cat: r = 2 .bird: r = 3 } match e { .count |c|: r = r + c .empty: r = r + 7 } return r } ` 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) testing.expect(t, strings.contains(llvm_text, "icmp eq")) // the call subject still dispatches } @(test) match_contextual_payload_variant_is_diagnosed :: proc(t: ^testing.T) { // Contextual construction is only for void variants; a bare `.point` for a payload // variant must use `Box{ point = ... }` instead. text := `Point :: struct { x i32 y i32 } Box :: union(enum) { point Point empty void } bad func() i32 { e Box = .point return 0 } main func() i32 { return bad() } ` 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "needs a payload") } testing.expect(t, found) } @(test) yield_misuse_is_diagnosed :: proc(t: ^testing.T) { // A value block that does not end in `yield`, and a `yield` nested inside an // `if` within a value block (only the final statement may yield). text := `main func() i32 { missing :: { k :: 5 } nested :: { if (true) { yield 1 } yield 2 } return missing + nested } ` 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_yield := false misplaced_yield := false for diagnostic in diagnostics.items { missing_yield = missing_yield || strings.contains(diagnostic.message, "a value block must end with an explicit 'yield'") misplaced_yield = misplaced_yield || strings.contains(diagnostic.message, "'yield' is only valid as the final statement of a value block") } testing.expect(t, missing_yield) testing.expect(t, misplaced_yield) } @(test) yield_control_flow_is_diagnosed :: proc(t: ^testing.T) { // Value if/loop/block misuse: an `if` value without an `else`; a branch that neither // yields nor exits on every path (20.6); a value loop whose body lacks a trailing // fall-through `yield`; a `yield :label` with no matching value loop; a labeled value // block that does not yield on every path; a `break :label` naming no loop; and a // `continue :label` targeting a block (not a loop). text := `main func() i32 { noelse :: if (true) { yield 1 } badbranch :: if (true) { k :: 5 } else { yield 2 } noloopyield :: for 0..10 |i| blk: { if (i == 0) yield :blk i } for 0..10 |j| stray: { yield :stray j } noblockyield :: blk: { if (true) yield :blk 1 k2 :: 5 } for 0..10 |m| { break :nope } scope: { continue :scope } return noelse + badbranch + noloopyield + noblockyield } ` 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) no_else := false branch_no_yield := false loop_no_yield := false stray_label := false block_no_yield := false bad_break_label := false continue_block := false for diagnostic in diagnostics.items { no_else = no_else || strings.contains(diagnostic.message, "an 'if' used as a value must have an 'else'") branch_no_yield = branch_no_yield || strings.contains(diagnostic.message, "a value branch must end with 'yield' or exit on every path") loop_no_yield = loop_no_yield || strings.contains(diagnostic.message, "a value loop's body must end with a 'yield'") stray_label = stray_label || strings.contains(diagnostic.message, "no enclosing value loop or block is labeled 'stray'") block_no_yield = block_no_yield || strings.contains(diagnostic.message, "a labeled value block must 'yield' on every path") bad_break_label = bad_break_label || strings.contains(diagnostic.message, "no enclosing loop is labeled 'nope'") // `continue :scope` targets a labeled block, which is not a loop. continue_block = continue_block || strings.contains(diagnostic.message, "no enclosing loop is labeled 'scope'") } testing.expect(t, no_else) testing.expect(t, branch_no_yield) testing.expect(t, loop_no_yield) testing.expect(t, stray_label) testing.expect(t, block_no_yield) testing.expect(t, bad_break_label) testing.expect(t, continue_block) } @(test) foreign_function_links_from_c_source :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-source" defer _ = os.remove(output) arguments := []linker.Argument{{kind=.Input, value="examples/interop/manual/native.c"}} status := compiler_core.compile_package("examples/interop/manual", output, arguments) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 42) } @(test) restricted_c_header_imports_compile_and_link :: proc(t: ^testing.T) { output := "/tmp/brolang-test-header-import" defer _ = os.remove(output) arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}} c_options := cimport.Options{ include_paths=[]string{"examples/interop/header/include"}, defines=[]string{"BROLANG_FEATURE"}, } status := compiler_core.compile_package("examples/interop/header/app", output, arguments, target.DEFAULT, c_options) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) by_value_c_records_and_unions_compile_and_link :: proc(t: ^testing.T) { output := "/tmp/brolang-test-records" defer _ = os.remove(output) arguments := []linker.Argument{{kind=.Input, value="examples/interop/records/native.c"}} c_options := cimport.Options{include_paths=[]string{"examples/interop/records/include"}} status := compiler_core.compile_package("examples/interop/records/app", output, arguments, target.DEFAULT, c_options) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 1) } @(test) self_referential_c_records_import_and_compile :: proc(t: ^testing.T) { // `struct Node { struct Node *next; int value; }`: the importer must not recurse // forever populating the self-referential record, and the checker must accept the // self-referential `?*mut Node` field as C-layout-compatible. output := "/tmp/brolang-test-recursive" defer _ = os.remove(output) c_options := cimport.Options{include_paths=[]string{"examples/interop/recursive/include"}} status := compiler_core.compile_package("examples/interop/recursive/app", output, nil, target.DEFAULT, c_options) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) unsupported_c_header_members_diagnose_only_when_referenced :: proc(t: ^testing.T) { output := "/tmp/brolang-test-header-unsupported" defer _ = os.remove(output) c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}} status := compiler_core.compile_package("examples/interop/header_unsupported", output, nil, target.DEFAULT, c_options) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) compatible_c_header_redeclarations_share_one_llvm_declaration :: proc(t: ^testing.T) { output := "/tmp/brolang-test-header-duplicate" defer _ = os.remove(output) arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}} c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}} status := compiler_core.compile_package("examples/interop/header_duplicate", output, arguments, target.DEFAULT, c_options) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) interop_foundation_matches_zig_compiled_apple_silicon_c_fixture :: proc(t: ^testing.T) { output := "/tmp/brolang-test-interop-foundation" defer _ = os.remove(output) arguments := []linker.Argument{{kind=.Input, value="examples/interop/foundation/native.c"}} status := compiler_core.compile_package("examples/interop/foundation", output, arguments) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 1) } @(test) foreign_function_links_from_object :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-object" object := "/tmp/brolang-test-foreign-object.o" defer _ = os.remove(output) defer _ = os.remove(object) testing.expect(t, prepare_native_object(object)) arguments := []linker.Argument{{kind=.Input, value=object}} status := compiler_core.compile_package("examples/interop/manual", output, arguments) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 42) } @(test) foreign_function_links_from_direct_library :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-direct-library" object := "/tmp/brolang-test-foreign-direct-library.o" archive := "/tmp/brolang-test-foreign-direct-library.a" defer _ = os.remove(output) defer _ = os.remove(object) defer _ = os.remove(archive) testing.expect(t, prepare_native_object(object)) testing.expect(t, prepare_native_archive(object, archive)) arguments := []linker.Argument{{kind=.Input, value=archive}} status := compiler_core.compile_package("examples/interop/manual", output, arguments) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 42) } @(test) foreign_function_links_from_named_library :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-named-library" object := "/tmp/brolang-test-foreign-named-library.o" archive := "/tmp/libbrolang-test-foreign-named.a" defer _ = os.remove(output) defer _ = os.remove(object) defer _ = os.remove(archive) testing.expect(t, prepare_native_object(object)) testing.expect(t, prepare_native_archive(object, archive)) arguments := []linker.Argument{ {kind=.Library_Path, value="/tmp"}, {kind=.Library, value="brolang-test-foreign-named"}, } status := compiler_core.compile_package("examples/interop/manual", output, arguments) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 42) } @(test) one_line_main_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-one-line-main" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/one_line", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 7) } @(test) folded_constant_addition_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-constant-fold" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/constant_fold", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) unused_invalid_global_does_not_trap :: proc(t: ^testing.T) { output := "/tmp/brolang-test-invalid-unused" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/invalid_unused_global", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) used_invalid_global_traps :: proc(t: ^testing.T) { output := "/tmp/brolang-test-invalid-used" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/invalid_used_global", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) main_int_is_constrained_to_i32 :: proc(t: ^testing.T) { output := "/tmp/brolang-test-main-int" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/main_int", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 3) } @(test) main_i32_returns_directly :: proc(t: ^testing.T) { output := "/tmp/brolang-test-main-i32" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/main_i32", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 4) } @(test) transitive_problematic_global_is_deferred :: proc(t: ^testing.T) { output := "/tmp/brolang-test-transitive-unused" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/invalid_transitive_unused_global", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) checked_addition_traps_on_overflow :: proc(t: ^testing.T) { output := "/tmp/brolang-test-overflow" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/overflow", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect(t, !state.success) } @(test) checked_runtime_negation_traps_for_every_signed_width :: proc(t: ^testing.T) { Case :: struct { type_name: string, magnitude: string, } cases := [?]Case{ {type_name="i8", magnitude="128"}, {type_name="i16", magnitude="32768"}, {type_name="i32", magnitude="2147483648"}, {type_name="i64", magnitude="9223372036854775808"}, } for test_case in cases { directory := fmt.aprintf("/tmp/brolang-test-negate-overflow-%s", test_case.type_name) main_path := fmt.aprintf("%s/main.bro", directory) output := fmt.aprintf("/tmp/brolang-test-negate-overflow-output-%s", test_case.type_name) builder := strings.builder_make() fmt.sbprintf( &builder, "negate func(value %s) %s {{ return -value }}\nmain func() void {{ _ = negate(-%s) }}\n", test_case.type_name, test_case.type_name, test_case.magnitude, ) text := strings.clone(strings.to_string(builder)) strings.builder_destroy(&builder) _ = os2.remove_all(directory) _ = os.remove(output) testing.expect(t, os.make_directory(directory) == nil) testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) status := compiler_core.compile_package(directory, output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect(t, !state.success) _ = os.remove(output) _ = os2.remove_all(directory) delete(text) delete(output) delete(main_path) delete(directory) } } @(test) constant_that_does_not_fit_context_produces_trap_executable :: proc(t: ^testing.T) { output := "/tmp/brolang-test-constant-context-error" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/constant_context_error", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) constant_beyond_i64_produces_trap_executable :: proc(t: ^testing.T) { output := "/tmp/brolang-test-constant-i64-overflow" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/constant_i64_overflow", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) same_line_statements_are_diagnosed :: proc(t: ^testing.T) { text := "main func() void { _ = 1 _ = 2 }\n" 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) testing.expect(t, len(diagnostics.items) > 0) } @(test) missing_main_produces_trap_executable :: proc(t: ^testing.T) { output := "/tmp/brolang-test-missing-main" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/missing_main", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) backend_failure_preserves_existing_output :: proc(t: ^testing.T) { output := "/tmp/brolang-test-preserved-output" defer _ = os.remove(output) previous := "previous artifact" testing.expect(t, os.write_entire_file(output, transmute([]byte)previous)) testing.expect(t, !backend.compile("/definitely/not/llvm.ll", output)) data, ok := os.read_entire_file(output) defer delete(data) testing.expect(t, ok) testing.expect_value(t, string(data), previous) } @(test) backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T) { arguments := []linker.Argument{ {kind=.Input, value="native.c"}, {kind=.Library_Path, value="vendor/lib"}, {kind=.Library, value="thing"}, {kind=.Input, value="helper.o"}, } c_options := cimport.Options{ include_paths=[]string{"vendor/include"}, defines=[]string{"FEATURE=1"}, } command := backend.build_command("module.ll", "program", arguments, target.DEFAULT, c_options) defer backend.destroy_command(command) expected := []string{ "/usr/bin/env", "zig", "cc", "-target", "aarch64-macos", "-Wno-override-module", "-Wno-unused-command-line-argument", "module.ll", "-Ivendor/include", "-DFEATURE=1", "native.c", "-Lvendor/lib", "-lthing", "helper.o", "-o", "program", } testing.expect_value(t, len(command), len(expected)) for value, index in expected { testing.expect_value(t, command[index], value) } } Fake_Cimport_State :: struct { calls: int, available: bool, infrastructure: bool, saw_options: bool, } Conflict_Cimport_State :: struct { calls: int, } Symbol_Conflict_Cimport_State :: struct { calls: int, } fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result { state := (^Fake_Cimport_State)(user_data) state.calls += 1 state.saw_options = len(request.include_paths) == 2 && request.include_paths[0] == "first/include" && request.include_paths[1] == "second/include" && len(request.defines) == 2 && request.defines[0] == "FIRST=1" && request.defines[1] == "SECOND" && request.target == target.DEFAULT result := cimport.init_result(allocator) if !state.available { result.infrastructure = state.infrastructure result.error_message = fmt.aprintf("fake importer unavailable", allocator=allocator) return result } append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE}) append(&result.functions, cimport.Function{ name=fmt.aprintf("fake_value", allocator=allocator), result=cimport.Type_Id(0), variadic=true, reason=fmt.aprintf("", allocator=allocator), }) append(&result.variables, cimport.Variable{ name=fmt.aprintf("fake_global", allocator=allocator), type=cimport.Type_Id(0), mutable=true, reason=fmt.aprintf("", allocator=allocator), }) append(&result.macros, cimport.Macro_Constant{ name=fmt.aprintf("FAKE_MAGIC", allocator=allocator), type=cimport.Type_Id(0), value={kind=.Integer, type=cimport.Type_Id(0), integer=7}, reason=fmt.aprintf("", allocator=allocator), }) result.available = true return result } conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result { state := (^Conflict_Cimport_State)(user_data) state.calls += 1 result := cimport.init_result(allocator) kind := cimport.Type_Kind.C_Int if strings.has_suffix(request.path, "second.h") { kind = .C_Long } append(&result.types, cimport.Type{kind=kind, child=cimport.INVALID_TYPE}) append(&result.variables, cimport.Variable{ name=fmt.aprintf("conflict_global", allocator=allocator), type=cimport.Type_Id(0), mutable=true, reason=fmt.aprintf("", allocator=allocator), }) result.available = true return result } symbol_conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result { state := (^Symbol_Conflict_Cimport_State)(user_data) state.calls += 1 result := cimport.init_result(allocator) append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE}) if strings.has_suffix(request.path, "variable.h") { append(&result.variables, cimport.Variable{ name=fmt.aprintf("conflict_symbol", allocator=allocator), type=cimport.Type_Id(0), mutable=true, reason=fmt.aprintf("", allocator=allocator), }) } else { append(&result.functions, cimport.Function{ name=fmt.aprintf("conflict_symbol", allocator=allocator), result=cimport.Type_Id(0), reason=fmt.aprintf("", allocator=allocator), }) } result.available = true return result } main_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result { result := cimport.init_result(allocator) append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE}) append(&result.variables, cimport.Variable{ name=fmt.aprintf("main", allocator=allocator), type=cimport.Type_Id(0), mutable=true, reason=fmt.aprintf("", allocator=allocator), }) result.available = true return result } write_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result { result := cimport.init_result(allocator) append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE}) append(&result.variables, cimport.Variable{ name=fmt.aprintf("write", allocator=allocator), type=cimport.Type_Id(0), mutable=true, reason=fmt.aprintf("", allocator=allocator), }) result.available = true return result } count_substring_occurrences :: proc(text, needle: string) -> int { if len(needle) == 0 { return 0 } count := 0 for index := 0; index + len(needle) <= len(text); index += 1 { if text[index:index + len(needle)] == needle { count += 1 } } 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 { return &variable, true } } return nil, false } count_cimport_variables :: proc(result: ^cimport.Result, name: string) -> int { count := 0 for variable in result.variables { if variable.name == name { count += 1 } } return count } find_cimport_macro :: proc(result: ^cimport.Result, name: string) -> (^cimport.Macro_Constant, bool) { for ¯o in result.macros { if macro.name == name { return ¯o, true } } return nil, false } cimport_has_named_result :: proc(result: ^cimport.Result, name: string) -> bool { if _, ok := find_cimport_macro(result, name); ok { return true } for item in result.unsupported { if item.name == name { return true } } return false } @(test) cimport_backend_is_replaceable :: proc(t: ^testing.T) { state := Fake_Cimport_State{available=true} options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}} result := cimport.import_header(options, "fake.h") defer cimport.destroy_result(&result) testing.expect(t, result.available) testing.expect_value(t, state.calls, 1) testing.expect_value(t, len(result.functions), 1) testing.expect_value(t, len(result.variables), 1) testing.expect_value(t, len(result.macros), 1) testing.expect_value(t, result.functions[0].name, "fake_value") testing.expect_value(t, result.variables[0].name, "fake_global") testing.expect_value(t, result.macros[0].name, "FAKE_MAGIC") testing.expect(t, result.functions[0].variadic) } @(test) libclang_import_preserves_external_object_and_final_macro_semantics :: proc(t: ^testing.T) { options := cimport.Options{ include_paths=[]string{"examples/interop/header/include"}, defines=[]string{"BROLANG_FEATURE"}, } result := cimport.import_header( options, "examples/interop/header/include/native.h", target.DEFAULT, ) defer cimport.destroy_result(&result) testing.expect(t, result.available) testing.expect_value(t, result.error_message, "") enum_alias_type := cimport.INVALID_TYPE for alias in result.aliases { if alias.name == "Imported_Enum" { enum_alias_type = alias.type break } } testing.expect(t, enum_alias_type != cimport.INVALID_TYPE) if enum_alias_type != cimport.INVALID_TYPE { testing.expect_value(t, result.types[enum_alias_type].kind, cimport.Type_Kind.C_Int) } enum_negative, found_enum_negative := find_cimport_macro(&result, "IMPORTED_ENUM_NEGATIVE") enum_same, found_enum_same := find_cimport_macro(&result, "IMPORTED_ENUM_SAME") enum_value, found_enum_value := find_cimport_macro(&result, "IMPORTED_ENUM_VALUE") enum_back, found_enum_back := find_cimport_macro(&result, "IMPORTED_ENUM_BACK") enum_anon, found_enum_anon := find_cimport_macro(&result, "IMPORTED_ANON_ENUM") testing.expect(t, found_enum_negative && found_enum_same && found_enum_value && found_enum_back && found_enum_anon) if found_enum_negative { testing.expect(t, enum_negative.value.negative) testing.expect_value(t, enum_negative.value.integer, u64(2)) } if found_enum_same { testing.expect(t, enum_same.value.negative) testing.expect_value(t, enum_same.value.integer, u64(2)) } if found_enum_value { testing.expect(t, !enum_value.value.negative) testing.expect_value(t, enum_value.value.integer, u64(7)) } if found_enum_back { testing.expect_value(t, enum_back.value.integer, u64(3)) } if found_enum_anon { testing.expect_value(t, enum_anon.value.integer, u64(9)) } tls, found_tls := find_cimport_variable(&result, "imported_tls_global") testing.expect(t, found_tls) if found_tls { testing.expect(t, strings.contains(tls.reason, "thread-local C variables are not supported")) } const_array, found_const_array := find_cimport_variable(&result, "imported_const_array") testing.expect(t, found_const_array) if found_const_array { testing.expect(t, !const_array.mutable) testing.expect(t, const_array.type != cimport.INVALID_TYPE) if const_array.type != cimport.INVALID_TYPE { array_type := result.types[const_array.type] testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array) testing.expect(t, array_type.child != cimport.INVALID_TYPE) if array_type.child != cimport.INVALID_TYPE { testing.expect_value(t, result.types[array_type.child].kind, cimport.Type_Kind.C_Int) } } } typedef_const_array, found_typedef_const_array := find_cimport_variable( &result, "imported_typedef_const_array", ) testing.expect(t, found_typedef_const_array) if found_typedef_const_array { testing.expect(t, !typedef_const_array.mutable) testing.expect_value(t, typedef_const_array.reason, "") testing.expect(t, typedef_const_array.type != cimport.INVALID_TYPE) if typedef_const_array.type != cimport.INVALID_TYPE { array_type := result.types[typedef_const_array.type] testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array) testing.expect(t, !array_type.mutable) testing.expect_value(t, array_type.count, u64(2)) } } redeclared_array, found_redeclared_array := find_cimport_variable( &result, "imported_redeclared_array", ) testing.expect(t, found_redeclared_array) testing.expect_value(t, count_cimport_variables(&result, "imported_redeclared_array"), 1) if found_redeclared_array { testing.expect_value(t, redeclared_array.reason, "") testing.expect(t, redeclared_array.type != cimport.INVALID_TYPE) if redeclared_array.type != cimport.INVALID_TYPE { array_type := result.types[redeclared_array.type] testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array) testing.expect_value(t, array_type.count, u64(4)) } } repeated, found_repeated := find_cimport_macro(&result, "IMPORTED_REPEAT") testing.expect(t, found_repeated) if found_repeated { testing.expect_value(t, repeated.value.integer, u64(123)) } testing.expect(t, !cimport_has_named_result(&result, "IMPORTED_GUARDED")) testing.expect(t, !cimport_has_named_result(&result, "IMPORTED_ONCE")) negative_decimal, found_negative_decimal := find_cimport_macro(&result, "IMPORTED_NEG_DECIMAL") testing.expect(t, found_negative_decimal) if found_negative_decimal { testing.expect_value(t, result.types[negative_decimal.type].kind, cimport.Type_Kind.C_Long) testing.expect_value(t, negative_decimal.value.integer, u64(2147483648)) testing.expect(t, negative_decimal.value.negative) } negative_hex, found_negative_hex := find_cimport_macro(&result, "IMPORTED_NEG_HEX") testing.expect(t, found_negative_hex) if found_negative_hex { testing.expect_value(t, result.types[negative_hex.type].kind, cimport.Type_Kind.C_Uint) testing.expect_value(t, negative_hex.value.integer, u64(0x80000000)) testing.expect(t, !negative_hex.value.negative) } negative_uint, found_negative_uint := find_cimport_macro(&result, "IMPORTED_NEG_UINT") testing.expect(t, found_negative_uint) if found_negative_uint { testing.expect_value(t, result.types[negative_uint.type].kind, cimport.Type_Kind.C_Uint) testing.expect_value(t, negative_uint.value.integer, u64(0xffffffff)) testing.expect(t, !negative_uint.value.negative) } conversions, found_conversions := find_cimport_macro(&result, "IMPORTED_CONVERSIONS") testing.expect(t, found_conversions) if found_conversions { testing.expect_value(t, len(conversions.values), 5) expected_kinds := [?]cimport.Type_Kind{ .C_Int, .C_Double, .C_Int, .C_Float, .C_Int, } for value, index in conversions.values { testing.expect(t, value.type != cimport.INVALID_TYPE) if value.type != cimport.INVALID_TYPE { testing.expect_value(t, result.types[value.type].kind, expected_kinds[index]) } } } } @(test) final_macros_override_same_named_c_value_declarations :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) c_options := cimport.Options{ include_paths=[]string{"examples/interop/header/include"}, defines=[]string{"BROLANG_FEATURE"}, } module, loaded := loader.load( "examples/interop/header/app", &sources, &diagnostics, &symbols, c_options=c_options, ) defer ast.destroy_module(&module) testing.expect(t, loaded) hir_module := checker.check(&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) testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_OBJECT = external")) testing.expect(t, !strings.contains(llvm_text, "declare i32 @IMPORTED_SHADOW_FUNCTION(")) testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_UNSUPPORTED = external")) testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_EMPTY_SHADOW = external")) } @(test) static_inline_c_functions_route_through_generated_trampolines :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) c_options := cimport.Options{ include_paths=[]string{"examples/interop/header/include"}, defines=[]string{"BROLANG_FEATURE"}, } module, loaded := loader.load( "examples/interop/header/app", &sources, &diagnostics, &symbols, c_options=c_options, ) defer ast.destroy_module(&module) testing.expect(t, loaded) // Symbols are namespaced by a header-path hash, so match by suffix. scalar_symbol := "" record_symbol := "" for trampoline in module.c_trampolines { testing.expect(t, strings.has_prefix(trampoline.symbol, "__brolang_inline_")) if strings.has_suffix(trampoline.symbol, "_imported_inline") { scalar_symbol = trampoline.symbol } if strings.has_suffix(trampoline.symbol, "_imported_inline_record") { record_symbol = trampoline.symbol } // The variadic static inline is unsupported and must not be wrapped. testing.expect(t, !strings.has_suffix(trampoline.symbol, "_imported_inline_variadic")) } testing.expect(t, scalar_symbol != "") testing.expect(t, record_symbol != "") // A static inline whose signature translates but is rejected by the loader's // by-value layout checks keeps its cimport-assigned link_name yet must not // emit a wrapper — it is uncallable, so the wrapper would be dead code. bad_layout_link := "" for function in module.functions { if symbol.resolve(&symbols, function.name) == "imported_inline_bad_layout" { testing.expect(t, len(function.unsupported_reason) > 0) bad_layout_link = function.link_name } } testing.expect(t, bad_layout_link != "") // cimport did generate a wrapper symbol for trampoline in module.c_trampolines { testing.expect(t, trampoline.symbol != bad_layout_link) } hir_module := checker.check(&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) testing.expect(t, strings.contains(llvm_text, fmt.tprintf("@%s(", scalar_symbol))) testing.expect(t, strings.contains(llvm_text, fmt.tprintf("@%s(", record_symbol))) // The internal-linkage C symbol itself is never declared or called directly. testing.expect(t, !strings.contains(llvm_text, "@imported_inline(")) } @(test) loader_injects_and_caches_cimport_backend_per_compilation :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) state := Fake_Cimport_State{available=true} options := cimport.Options{ include_paths=[]string{"first/include", "second/include"}, defines=[]string{"FIRST=1", "SECOND"}, backend={import_header=fake_cimport_backend, user_data=&state}, } module, loaded := loader.load( "examples/interop/header_cache", &sources, &diagnostics, &symbols, c_options=options, ) defer ast.destroy_module(&module) testing.expect(t, loaded) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, state.calls, 1) testing.expect(t, state.saw_options) testing.expect_value(t, len(module.imports), 2) testing.expect_value(t, module.imports[0].target, module.imports[1].target) testing.expect_value(t, len(module.functions), 2) found_variadic := false for function in module.functions { found_variadic = found_variadic || function.variadic } testing.expect(t, found_variadic) found_external := false found_macro := false for global in module.globals { name := symbol.resolve(&symbols, global.name) found_external = found_external || (name == "fake_global" && global.external && global.writable) found_macro = found_macro || name == "FAKE_MAGIC" } testing.expect(t, found_external) testing.expect(t, found_macro) } @(test) conflicting_external_c_globals_are_diagnosed_and_deduped :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) state := Conflict_Cimport_State{} options := cimport.Options{backend={import_header=conflict_cimport_backend, user_data=&state}} module, loaded := loader.load( "examples/interop/header_conflict", &sources, &diagnostics, &symbols, c_options=options, ) defer ast.destroy_module(&module) testing.expect(t, loaded) testing.expect_value(t, state.calls, 2) hir_module := checker.check(&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) found_conflict := false for diagnostic in diagnostics.items { if strings.contains(diagnostic.message, "conflicting external C variable declarations for 'conflict_global'") { found_conflict = true } } testing.expect(t, found_conflict) testing.expect_value(t, count_substring_occurrences(llvm_text, "@conflict_global = external global"), 1) testing.expect(t, strings.contains(llvm_text, "@conflict_global = external global i32")) testing.expect(t, !strings.contains(llvm_text, "@conflict_global = external global i64")) } @(test) external_c_global_and_function_link_name_conflict_is_diagnosed :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) state := Symbol_Conflict_Cimport_State{} options := cimport.Options{backend={import_header=symbol_conflict_cimport_backend, user_data=&state}} module, loaded := loader.load( "examples/interop/header_symbol_conflict", &sources, &diagnostics, &symbols, c_options=options, ) defer ast.destroy_module(&module) testing.expect(t, loaded) testing.expect_value(t, state.calls, 2) hir_module := checker.check(&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) found_conflict := false for diagnostic in diagnostics.items { if strings.contains(diagnostic.message, "external C variable 'conflict_symbol' conflicts with a C function declaration") { found_conflict = true } } testing.expect(t, found_conflict) testing.expect(t, !strings.contains(llvm_text, "@conflict_symbol = external global")) testing.expect(t, strings.contains(llvm_text, "declare i32 @conflict_symbol()")) testing.expect(t, !strings.contains(llvm_text, "load i32, ptr @conflict_symbol")) } @(test) external_c_global_named_main_is_omitted_for_root_entry_point :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) options := cimport.Options{backend={import_header=main_conflict_cimport_backend}} module, loaded := loader.load( "examples/interop/header_main_conflict", &sources, &diagnostics, &symbols, c_options=options, ) defer ast.destroy_module(&module) testing.expect(t, loaded) hir_module := checker.check(&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) found_conflict := false for diagnostic in diagnostics.items { found_conflict = found_conflict || strings.contains( diagnostic.message, "external C variable 'main' conflicts with the program entry point", ) } testing.expect(t, found_conflict) testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main("), 1) testing.expect(t, !strings.contains(llvm_text, "@main = external")) } @(test) external_c_global_named_main_is_omitted_for_synthesized_entry_point :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) options := cimport.Options{backend={import_header=main_conflict_cimport_backend}} module, loaded := loader.load( "examples/interop/header_main_conflict_missing", &sources, &diagnostics, &symbols, c_options=options, ) defer ast.destroy_module(&module) testing.expect(t, loaded) hir_module := checker.check(&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) found_conflict := false found_missing_main := false for diagnostic in diagnostics.items { found_conflict = found_conflict || strings.contains( diagnostic.message, "external C variable 'main' conflicts with the program entry point", ) found_missing_main = found_missing_main || strings.contains(diagnostic.message, "missing or unusable main function") } testing.expect(t, found_conflict) testing.expect(t, found_missing_main) testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main("), 1) testing.expect(t, !strings.contains(llvm_text, "@main = external")) } @(test) external_c_global_named_write_is_omitted_for_compiler_runtime :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) options := cimport.Options{backend={import_header=write_conflict_cimport_backend}} module, loaded := loader.load( "examples/interop/header_write_conflict", &sources, &diagnostics, &symbols, c_options=options, ) defer ast.destroy_module(&module) testing.expect(t, loaded) hir_module := checker.check(&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) found_conflict := false for diagnostic in diagnostics.items { found_conflict = found_conflict || strings.contains( diagnostic.message, "external C variable 'write' conflicts with the compiler runtime", ) } testing.expect(t, found_conflict) testing.expect_value(t, count_substring_occurrences(llvm_text, "declare i64 @write("), 1) testing.expect(t, !strings.contains(llvm_text, "@write = external")) testing.expect(t, strings.contains(llvm_text, "call void @bro.trap")) } @(test) tls_reference_and_const_external_array_assignment_are_diagnosed :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}} module, loaded := loader.load( "examples/interop/header_unsupported", &sources, &diagnostics, &symbols, c_options=c_options, ) defer ast.destroy_module(&module) testing.expect(t, loaded) hir_module := checker.check(&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) found_tls := false found_excess_aggregate := false found_signed_narrow := false found_shadowed_unsupported := false found_float_overflow := false found_empty_shadow := false found_inline_variadic := false not_writable_count := 0 for diagnostic in diagnostics.items { found_tls = found_tls || strings.contains( diagnostic.message, "C declaration 'imported_tls_global' is unavailable: thread-local C variables are not supported", ) found_excess_aggregate = found_excess_aggregate || strings.contains( diagnostic.message, "C declaration 'IMPORTED_TOO_MANY_COLOR' is unavailable: C macro aggregate initializer is not representable", ) found_signed_narrow = found_signed_narrow || strings.contains( diagnostic.message, "C declaration 'IMPORTED_SIGNED_NARROW_BAD' is unavailable: C macro aggregate initializer is not representable", ) found_shadowed_unsupported = found_shadowed_unsupported || strings.contains( diagnostic.message, "C declaration 'IMPORTED_SHADOW_UNSUPPORTED' is unavailable: C macro is not a supported constant", ) found_float_overflow = found_float_overflow || strings.contains( diagnostic.message, "C declaration 'IMPORTED_FLOAT_OVERFLOW' is unavailable: C macro is not a supported constant", ) found_empty_shadow = found_empty_shadow || strings.contains( diagnostic.message, "C declaration 'IMPORTED_EMPTY_SHADOW' is unavailable: C macro has no replacement value", ) found_inline_variadic = found_inline_variadic || strings.contains( diagnostic.message, "C declaration 'imported_inline_variadic' is unavailable: variadic static inline C functions are not supported", ) if strings.contains(diagnostic.message, "assignment target is not writable") { not_writable_count += 1 } } testing.expect(t, found_tls) testing.expect(t, found_excess_aggregate) testing.expect(t, found_signed_narrow) testing.expect(t, found_shadowed_unsupported) testing.expect(t, found_float_overflow) testing.expect(t, found_empty_shadow) testing.expect(t, found_inline_variadic) testing.expect(t, not_writable_count >= 5) testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_EMPTY_SHADOW = external")) testing.expect( t, strings.contains( llvm_text, "@imported_typedef_const_array = external constant [2 x i32]", ), ) testing.expect_value( t, count_substring_occurrences( llvm_text, "@imported_redeclared_array = external global [4 x i32]", ), 1, ) testing.expect( t, !strings.contains( llvm_text, "ptr @imported_const_array_record, i64 0", ), ) testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_UNSUPPORTED = external")) } @(test) cimport_infrastructure_failure_makes_compilation_unavailable :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) state := Fake_Cimport_State{infrastructure=true} options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}} module, loaded := loader.load( "examples/interop/header_cache", &sources, &diagnostics, &symbols, c_options=options, ) defer ast.destroy_module(&module) testing.expect(t, !loaded) testing.expect_value(t, state.calls, 1) testing.expect(t, len(diagnostics.items) > 0) } @(test) source_store_owns_buffers_indexes_lines_and_deduplicates_diagnostics :: proc(t: ^testing.T) { store := source.init_store() defer source.destroy_store(&store) bytes := make([]byte, len("one\ntwo\n")) copy(bytes, "one\ntwo\n") source_id := source.add_source_owned(&store, "owned.bro", bytes) bytes[0] = 'O' diagnostics := source.init_store_diagnostics(&store) defer source.destroy_diagnostics(&diagnostics) span := source.Span{file=source_id, start=4, end=7} first := source.add(&diagnostics, span, "same") second := source.addf(&diagnostics, span, "%s", "same") other := source.add(&diagnostics, span, "other") formatted := source.format(&diagnostics, other) defer delete(formatted) testing.expect_value(t, store.items[source_id].text, "One\ntwo\n") testing.expect_value(t, len(store.items[source_id].line_starts), 3) testing.expect_value(t, first, second) testing.expect_value(t, len(diagnostics.items), 2) testing.expect(t, strings.contains(formatted, "owned.bro:2:1:")) } @(test) maximum_signed_i64_literal_parses_exactly :: proc(t: ^testing.T) { source_file := source.Source{path="test.bro", text="value :: 9223372036854775807\nmain func() void {}\n"} 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(9223372036854775807)) } @(test) negative_constants_fold_and_accept_signed_i64_minimum :: proc(t: ^testing.T) { text := `minimum :: -9223372036854775808 grouped i64 :: -(9223372036854775808) folded :: -(1 + 2) main func() void {} ` 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, types.equal(hir_module.globals[0].type, types.I64)) testing.expect(t, types.equal(hir_module.globals[1].type, types.I64)) testing.expect(t, types.equal(hir_module.globals[2].type, types.I8)) testing.expect_value(t, hir_module.globals[0].static_value, i64(-9223372036854775807-1)) testing.expect_value(t, hir_module.globals[1].static_value, i64(-9223372036854775807-1)) testing.expect_value(t, hir_module.globals[2].static_value, i64(-3)) } @(test) constant_division_by_zero_has_a_precise_diagnostic :: proc(t: ^testing.T) { text := `value :: 5 / 0 main func() void {} ` 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) found_division_by_zero := false found_overflow := false for diagnostic in diagnostics.items { found_division_by_zero = found_division_by_zero || strings.contains(diagnostic.message, "division by zero in constant expression") found_overflow = found_overflow || strings.contains(diagnostic.message, "integer constant expression exceeds signed i64 range") } testing.expect(t, found_division_by_zero) testing.expect(t, !found_overflow) } @(test) out_of_range_negative_constants_are_diagnosed :: proc(t: ^testing.T) { text := `positive :: 9223372036854775808 below_minimum :: -9223372036854775809 double_minimum :: --9223372036854775808 maximum_u64 :: 18446744073709551615 beyond_u64 :: 18446744073709551616 main func() void {} ` 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) found_signed_range := 0 found_u64_range := false for diagnostic in diagnostics.items { found_signed_range += 1 if strings.contains(diagnostic.message, "exceeds signed i64 range") else 0 found_u64_range = found_u64_range || strings.contains(diagnostic.message, "magnitude does not fit in u64") } testing.expect_value(t, found_signed_range, 4) testing.expect(t, found_u64_range) } @(test) runtime_negation_preserves_operand_type_before_result_widening :: proc(t: ^testing.T) { text := `negate_i8 func(value i8) i8 { return -value } widen_after_negate func(value i8) i16 { return -value } main func() void { _ = negate_i8(1) _ = widen_after_negate(1) } ` 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) testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i8")) found_widen := false for function in hir_module.functions { if symbol.resolve(&symbols, function.name) != "widen_after_negate" { continue } statement := hir_module.statements[function.body[0]] widen := hir_module.exprs[statement.expr] negate := hir_module.exprs[widen.left] testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen) testing.expect(t, types.equal(widen.type, types.I16)) testing.expect_value(t, negate.kind, hir.Expr_Kind.Negate) testing.expect(t, types.equal(negate.type, types.I8)) found_widen = true } testing.expect(t, found_widen) } @(test) malformed_hir_references_lower_to_valid_trapped_llvm :: proc(t: ^testing.T) { hir_module := hir.init_module() defer hir.destroy_module(&hir_module) append(&hir_module.exprs, hir.Expr{ kind=.Local, type=types.I8, target=hir.local_ref(hir.INVALID_LOCAL), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) append(&hir_module.statements, hir.Stmt{ kind=.Expression, expr=hir.Expr_Id(0), local=hir.INVALID_LOCAL, diagnostic=source.INVALID_DIAGNOSTIC, }) body := make([]hir.Stmt_Id, 1) body[0] = hir.Stmt_Id(0) append(&hir_module.functions, hir.Function{ name=symbol.INVALID, link_name=strings.clone("main"), calling_convention=.C, implementation=.Definition, linkage=.External, is_main=true, result=types.VOID, body=body, diagnostic=source.INVALID_DIAGNOSTIC, }) ir_module := lower.lower(&hir_module) defer ir.destroy_module(&ir_module) source_file := source.Source{path="test.bro", text=""} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) text := llvm.emit(&ir_module, &diagnostics, &symbols) defer delete(text) testing.expect(t, strings.contains(text, "call void @bro.trap")) testing.expect(t, !strings.contains(text, "%v-1")) } @(test) malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) { module := ir.init_module() defer ir.destroy_module(&module) instructions := make([]ir.Instruction, 4) instructions[0] = ir.Instruction{op=.Store, type=types.I8, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} instructions[1] = ir.Instruction{op=.Add_Checked, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} instructions[2] = ir.Instruction{op=.Neg_Checked, type=types.I16, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} instructions[3] = ir.Instruction{op=.Return, type=types.I32, a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} append(&module.functions, ir.Function{ link_name=strings.clone("main"), calling_convention=.C, implementation=.Definition, linkage=.External, is_main=true, result=types.I32, instructions=instructions, }) source_file := source.Source{path="test.bro", text=""} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) text := llvm.emit(&module, &diagnostics, &symbols) defer delete(text) testing.expect(t, strings.contains(text, "call void @bro.trap")) testing.expect(t, strings.contains(text, "%v0 = add i8 0, -86")) testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766")) testing.expect(t, strings.contains(text, "%v2 = add i16 0, -21846")) testing.expect(t, strings.contains(text, "@bro.trap(ptr %message, i64 %length) noreturn")) testing.expect(t, !strings.contains(text, "%v-1")) llvm_path := "/tmp/brolang-test-malformed-recovery.ll" output := "/tmp/brolang-test-malformed-recovery" defer _ = os.remove(llvm_path) defer _ = os.remove(output) testing.expect(t, os.write_entire_file(llvm_path, transmute([]byte)text)) testing.expect(t, backend.compile(llvm_path, output)) } @(test) hundred_thousand_term_runtime_addition_uses_iterative_pipeline :: proc(t: ^testing.T) { builder := strings.builder_make() defer strings.builder_destroy(&builder) strings.write_string(&builder, "sum func(value i32) i32 { return value") for _ in 0..<100_000 { strings.write_string(&builder, " + 1") } strings.write_string(&builder, " }\nmain func() void { _ = sum(0) }\n") source_file := source.Source{path="test.bro", text=strings.to_string(builder)} 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) instruction_count := 0 for function in ir_module.functions { instruction_count += len(function.instructions) } testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, instruction_count > 100_000) } @(test) deep_global_cycle_detection_uses_iterative_dfs :: proc(t: ^testing.T) { count := 50_000 ast_module := ast.init_module() defer ast.destroy_module(&ast_module) source_file := source.Source{path="test.bro", text=""} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) name := symbol.intern(&symbols, "value") state := checker.Checker{ ast_module=&ast_module, diagnostics=&diagnostics, symbols=&symbols, module=hir.init_module(), allocator=context.allocator, } defer hir.destroy_module(&state.module) defer delete(state.cycle_stack) for id in 0.. 0 { return 1 } } main func() void { _ = classify(5) } ` 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "does not return a value") } testing.expect(t, found) } @(test) function_returning_in_both_if_arms_is_accepted :: proc(t: ^testing.T) { text := `classify func(n i32) i32 { if n > 0 { return 1 } else { return 0 } } main func() void { _ = classify(5) } ` 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "does not return a value") } testing.expect(t, !found) } @(test) function_returning_after_if_is_accepted :: proc(t: ^testing.T) { text := `classify func(n i32) i32 { if n > 0 { return 1 } return 0 } main func() void { _ = classify(5) } ` 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "does not return a value") } testing.expect(t, !found) } @(test) conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-conditional-unwrap" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/conditional_unwrap", output) testing.expect_value(t, status, 0) state := run_executable(output) // Single unwrap, guarded two/three-value unwraps, optional pointers, false // guards, and failed short-circuit chains preserve the expected total. testing.expect_value(t, state.exit_code, 42) } @(test) conditional_unwrap_parser_captures_guard_and_parenthesized_chain :: proc(t: ^testing.T) { text := `main func() void { first ?i32 = 1 second ?i32 = 2 if (first and second) |a, b : a == 1 and b == 2| { _ = a _ = b } } ` 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) testing.expect_value(t, len(diagnostics.items), 0) statement := module.statements[module.functions[0].body[2]] testing.expect_value(t, statement.kind, ast.Stmt_Kind.If) testing.expect_value(t, len(statement.captures), 2) testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.And) testing.expect(t, module.exprs[statement.expr].parenthesized) testing.expect(t, statement.guard != ast.INVALID_EXPR) testing.expect_value(t, module.exprs[statement.guard].kind, ast.Expr_Kind.And) } @(test) parser_accepts_braceless_if_bodies :: proc(t: ^testing.T) { text := `ready func() bool { return true } main func() void { x i32 = 0 if (x == 0) x = 1 if ready() x = 2 if (x == 2) x = 3 else x = 4 if (x > 0) { x = 10 } else x = 11 v ?i32 = 5 if v |u| _ = u } ` 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) testing.expect_value(t, len(diagnostics.items), 0) main := module.functions[1] testing.expect_value(t, len(main.body), 7) paren_if := module.statements[main.body[1]] testing.expect_value(t, paren_if.kind, ast.Stmt_Kind.If) testing.expect_value(t, len(paren_if.body), 1) testing.expect(t, module.exprs[paren_if.expr].parenthesized) call_if := module.statements[main.body[2]] testing.expect_value(t, call_if.kind, ast.Stmt_Kind.If) testing.expect_value(t, len(call_if.body), 1) testing.expect_value(t, module.exprs[call_if.expr].kind, ast.Expr_Kind.Call) if_else := module.statements[main.body[3]] testing.expect_value(t, len(if_else.body), 1) testing.expect_value(t, len(if_else.else_body), 1) braced_then := module.statements[main.body[4]] testing.expect_value(t, len(braced_then.body), 1) testing.expect_value(t, len(braced_then.else_body), 1) unwrap_if := module.statements[main.body[6]] testing.expect_value(t, len(unwrap_if.captures), 1) testing.expect_value(t, len(unwrap_if.body), 1) } @(test) parser_diagnoses_braceless_if_without_parens_or_call :: proc(t: ^testing.T) { text := `main func() void { x i32 = 0 if x == 0 x = 1 } ` 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) testing.expect_value(t, len(diagnostics.items), 1) testing.expect(t, strings.contains(diagnostics.items[0].message, "parenthesized")) } @(test) braceless_if_compiles_and_runs :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-braceless-if" main_path := "/tmp/brolang-test-braceless-if/main.bro" output := "/tmp/brolang-test-braceless-if-output" text := `ready func() bool { return true } main func() i32 { x i32 = 0 if (x == 0) x = 1 else x = 2 if ready() x = x + 10 y i32 = 5 if (y == 0) y = 1 else y = 30 x = x + y return x } ` _ = os2.remove_all(directory) defer _ = os2.remove_all(directory) defer _ = os.remove(output) testing.expect(t, os.make_directory(directory) == nil) testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) status := compiler_core.compile_package(directory, output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 41) } @(test) conditional_unwrap_allows_sink_captures :: proc(t: ^testing.T) { text := `main func() void { first ?i32 = 1 second ?i32 = 2 if first and second |_, value : value == 2| { _ = value } if first |_| {} } ` 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) testing.expect_value(t, len(diagnostics.items), 0) main := hir_module.functions[0] first_if := hir_module.statements[main.body[2]] second_if := hir_module.statements[main.body[3]] testing.expect_value(t, first_if.unwraps[0].local, hir.INVALID_LOCAL) testing.expect(t, first_if.unwraps[1].local != hir.INVALID_LOCAL) testing.expect_value(t, second_if.unwraps[0].local, hir.INVALID_LOCAL) } @(test) parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^testing.T) { cases := [4]struct { text: string, needle: string, }{ {`main func() void { value ?i32 = 1 if value || {} } `, "expected an unwrap capture name"}, {`main func() void { value ?i32 = 1 if value |capture,| {} } `, "expected an unwrap capture after ','"}, {`main func() void { value ?i32 = 1 if value |capture :| {} } `, "expected a guard expression after ':'"}, {`main func() void { value ?i32 = 1 if value |capture {} } `, "expected '|' to close unwrap 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) if_unwrap_on_non_optional_is_diagnosed :: proc(t: ^testing.T) { text := `main func() i32 { x i32 = 5 if x |v| { return v } 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "unwrap requires an optional") } testing.expect(t, found) } @(test) conditional_unwrap_diagnostics_cover_counts_guards_and_capture_scope :: proc(t: ^testing.T) { text := `main func() void { first ?i32 = 1 second ?i32 = 2 plain i32 = 3 if first and second |one| {} if first |one, two| {} if first and second |same, same| {} if plain |value| {} if first |value : value| {} if first and earlier |earlier, later| {} if first |value| { value = 2 } if first |value| { value i32 = 2 _ = value } if first |value| { _ = value } else { _ = value } _ = value } ` 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) count_mismatches := 0 duplicate := false non_optional := false guard := false outer_operand_scope := false immutable := false redeclaration := false capture_scope := 0 for diagnostic in diagnostics.items { count_mismatches += 1 if strings.contains(diagnostic.message, "unwrap has") else 0 duplicate = duplicate || strings.contains(diagnostic.message, "unwrap captures must have distinct names") non_optional = non_optional || strings.contains(diagnostic.message, "unwrap requires an optional value") guard = guard || strings.contains(diagnostic.message, "unwrap guard must be a bool") outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unresolved global 'earlier'") immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'") redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'value'") capture_scope += 1 if strings.contains(diagnostic.message, "unresolved global 'value'") else 0 } testing.expect_value(t, count_mismatches, 2) testing.expect(t, duplicate) testing.expect(t, non_optional) testing.expect(t, guard) testing.expect(t, outer_operand_scope) testing.expect(t, immutable) testing.expect(t, redeclaration) testing.expect_value(t, capture_scope, 2) } @(test) if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) { // The binding `v` is usable in the then-block but not in the else-block. text := `main func() i32 { a ?i32 = 1 if a |v| { return v } else { return v } } ` 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "unresolved global 'v'") } testing.expect(t, found) } @(test) if_unwrap_binding_is_immutable :: proc(t: ^testing.T) { text := `main func() i32 { a ?i32 = 1 if a |v| { v = 2 return v } 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "cannot assign immutable local 'v'") } 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) } @(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) range { 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() range { return 0..2 } make_array func() [2]i32 { 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")) } @(test) lexer_emits_compound_assignment_and_slash_tokens :: proc(t: ^testing.T) { source_file := source.Source{path="test.bro", 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) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, stream.items[0].kind, token.Kind.Plus_Equal) testing.expect_value(t, stream.items[1].kind, token.Kind.Minus_Equal) testing.expect_value(t, stream.items[2].kind, token.Kind.Star_Equal) testing.expect_value(t, stream.items[3].kind, token.Kind.Slash_Equal) testing.expect_value(t, stream.items[4].kind, token.Kind.Slash) testing.expect_value(t, stream.items[5].kind, token.Kind.Star) } @(test) binary_operators_respect_multiplicative_precedence :: proc(t: ^testing.T) { source_file := source.Source{path="test.bro", text="value :: 1 + 2 * 3\nmain func() void {}\n"} 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) root := module.exprs[module.globals[0].expr] testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, root.kind, ast.Expr_Kind.Add) testing.expect_value(t, module.exprs[root.left].integer, u64(1)) testing.expect_value(t, module.exprs[root.right].kind, ast.Expr_Kind.Mul) } @(test) division_parses_left_associatively :: proc(t: ^testing.T) { source_file := source.Source{path="test.bro", text="value :: 8 / 4 / 2\nmain func() void {}\n"} 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) root := module.exprs[module.globals[0].expr] testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, root.kind, ast.Expr_Kind.Div) testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Div) testing.expect_value(t, module.exprs[root.right].integer, u64(2)) } @(test) compound_assignment_preserves_operation_and_rhs :: proc(t: ^testing.T) { text := `main func() void { x i32 = 0 x += 5 } ` 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) testing.expect_value(t, len(diagnostics.items), 0) body := module.functions[0].body statement := module.statements[body[1]] testing.expect_value(t, statement.kind, ast.Stmt_Kind.Assignment) testing.expect_value(t, statement.assignment_op, ast.Assignment_Op.Add) testing.expect(t, statement.target != ast.INVALID_EXPR) testing.expect_value(t, module.exprs[statement.target].kind, ast.Expr_Kind.Name) testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Integer) testing.expect_value(t, module.exprs[statement.expr].integer, u64(5)) } @(test) undefined_inferred_local_lowers_to_fill :: proc(t: ^testing.T) { text := `choose func(flag bool) i32 { value int = undefined if flag { value = 42 } else { value = -2 } return value } main func() i32 { return choose(true) } ` 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) choose_symbol := symbol.intern(&symbols, "choose") value_symbol := symbol.intern(&symbols, "value") found_value_i32 := false fill_count := 0 for function, function_index in hir_module.functions { if function.name != choose_symbol { continue } for local in function.locals { found_value_i32 = found_value_i32 || local.name == value_symbol && local.type == types.I32 } for instruction in ir_module.functions[function_index].instructions { fill_count += 1 if instruction.op == .Fill else 0 } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found_value_i32) testing.expect_value(t, fill_count, 1) testing.expect(t, strings.contains(llvm_text, "declare void @llvm.memset.p0.i64")) testing.expect(t, strings.contains(llvm_text, "call void @llvm.memset.p0.i64")) testing.expect(t, strings.contains(llvm_text, "i8 -86")) } @(test) local_int_inference_widens_from_assignments :: proc(t: ^testing.T) { text := `wide func() int { value int = 1 value = 1000 return value } main func() void { _ = wide() } ` 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) wide_symbol := symbol.intern(&symbols, "wide") value_symbol := symbol.intern(&symbols, "value") found_value_i16 := false found_result_i16 := false for function in hir_module.functions { if function.name != wide_symbol { continue } found_result_i16 = function.result == types.I16 for local in function.locals { found_value_i16 = found_value_i16 || local.name == value_symbol && local.type == types.I16 } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found_result_i16) testing.expect(t, found_value_i16) } float_local_type :: proc(hir_module: ^hir.Module, symbols: ^symbol.Table, function_name, local_name: string) -> (types.Type, bool) { fn_symbol := symbol.intern(symbols, function_name) loc_symbol := symbol.intern(symbols, local_name) for function in hir_module.functions { if function.name != fn_symbol { continue } for local in function.locals { if local.name == loc_symbol { return local.type, true } } } return types.INVALID, false } float_result_type :: proc(hir_module: ^hir.Module, symbols: ^symbol.Table, function_name: string) -> (types.Type, bool) { fn_symbol := symbol.intern(symbols, function_name) for function in hir_module.functions { if function.name == fn_symbol { return function.result, true } } return types.INVALID, false } @(test) float_constraint_resolves_to_f64 :: proc(t: ^testing.T) { text := `make func() float { pi float = 3.14 return pi } main func() void { _ = make() } ` 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) pi_type, found := float_local_type(&hir_module, &symbols, "make", "pi") result_type, _ := float_result_type(&hir_module, &symbols, "make") testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found) testing.expect_value(t, pi_type, types.F64) testing.expect_value(t, result_type, types.F64) } @(test) float_constraint_accepts_integer_literal :: proc(t: ^testing.T) { text := `make func() float { pi float = 3 return pi } main func() void { _ = make() } ` 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) pi_type, found := float_local_type(&hir_module, &symbols, "make", "pi") testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found) testing.expect_value(t, pi_type, types.F64) } @(test) float_constraint_result_resolves_to_f64 :: proc(t: ^testing.T) { text := `make func() float { return 3.0 } main func() void { _ = make() } ` 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) result_type, found := float_result_type(&hir_module, &symbols, "make") testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found) testing.expect_value(t, result_type, types.F64) } @(test) float_constraint_widens_f32_to_f64 :: proc(t: ^testing.T) { text := `wide func(a f32, b f64) float { x float = a x = b return x } main func() void { _ = wide(1.0, 2.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) x_type, found := float_local_type(&hir_module, &symbols, "wide", "x") result_type, _ := float_result_type(&hir_module, &symbols, "wide") testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found) testing.expect_value(t, x_type, types.F64) testing.expect_value(t, result_type, types.F64) } @(test) int_constraint_rejects_float_initializer :: proc(t: ^testing.T) { text := `main func() void { x int = 1.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) found_constraint_error := false for diagnostic in diagnostics.items { found_constraint_error = found_constraint_error || strings.contains(diagnostic.message, "could not resolve the 'int' constraint for local 'x'") } testing.expect(t, found_constraint_error) } @(test) float_constraint_rejects_runtime_integer :: proc(t: ^testing.T) { text := `take func(n i32) void { x float = n } main func() void { take(7) } ` 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) found_convert_error := false for diagnostic in diagnostics.items { found_convert_error = found_convert_error || strings.contains(diagnostic.message, "cannot implicitly convert i32 to f64") } testing.expect(t, found_convert_error) } @(test) range_constraint_local_resolves_to_inferred_range :: proc(t: ^testing.T) { text := `make func() range { r range :: 0..10 return r } main func() void { _ = make() } ` 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) r_type, found := float_local_type(&hir_module, &symbols, "make", "r") result_type, _ := float_result_type(&hir_module, &symbols, "make") testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found) testing.expect(t, types.is_range(r_type, &hir_module.types)) testing.expect_value(t, types.child_type(r_type, &hir_module.types), types.I8) testing.expect(t, types.is_range(result_type, &hir_module.types)) } @(test) range_constraint_param_and_result_monomorphize :: proc(t: ^testing.T) { text := `pass func(r range) range { return r } main func() void { once :: 0..5 for pass(once) |v| { _ = v } } ` 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) result_type, found := float_result_type(&hir_module, &symbols, "pass") testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found) testing.expect(t, types.is_range(result_type, &hir_module.types)) } @(test) int_param_rejects_float_argument :: proc(t: ^testing.T) { text := `take func(x int) int { return x } main func() void { _ = take(1.5) } ` 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) found_reject := false for diagnostic in diagnostics.items { found_reject = found_reject || strings.contains(diagnostic.message, "cannot pass f64 to 'int' parameter 'x'") } testing.expect(t, found_reject) } @(test) float_param_accepts_integer_literal_argument :: proc(t: ^testing.T) { text := `take func(x float) float { return x } main func() void { y f64 = take(3) _ = y } ` 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) result_type, found := float_result_type(&hir_module, &symbols, "take") testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found) testing.expect_value(t, result_type, types.F64) } @(test) undefined_accepts_concrete_runtime_annotations :: proc(t: ^testing.T) { text := `Point :: struct { x i32 y i32 } main func() void { point Point = undefined pointer @i32 = undefined maybe ?i32 = undefined } ` 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) main_symbol := symbol.intern(&symbols, "main") fill_count := 0 for function, function_index in hir_module.functions { if function.name != main_symbol { continue } for instruction in ir_module.functions[function_index].instructions { fill_count += 1 if instruction.op == .Fill else 0 } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, fill_count, 3) } @(test) undefined_rejects_non_declaration_uses_and_unresolved_inference :: proc(t: ^testing.T) { text := `global :: undefined main func() void { immutable :: undefined typed_immutable int :: undefined unresolved int = undefined existing i32 = 1 existing = undefined mismatch int = undefined mismatch = 1 mismatch = 1.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) immutable_count := 0 found_unresolved := false found_assignment := false found_incompatible := false for diagnostic in diagnostics.items { immutable_count += 1 if strings.contains(diagnostic.message, "'undefined' requires a mutable local declaration") else 0 found_unresolved = found_unresolved || strings.contains(diagnostic.message, "could not infer a concrete type for local 'unresolved'") found_assignment = found_assignment || strings.contains(diagnostic.message, "'undefined' is only valid as a mutable local declaration initializer") found_incompatible = found_incompatible || strings.contains(diagnostic.message, "cannot implicitly convert f64 to i8") } testing.expect(t, immutable_count >= 2) testing.expect(t, found_unresolved) testing.expect(t, found_assignment) testing.expect(t, found_incompatible) } @(test) compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) { // A compound assignment to an indexed lvalue must compute the element address // once and reuse it for the load and the store, rather than re-lowering the // lvalue (which would re-evaluate any side-effecting index subexpression). text := `bump func() usize { return 1 } main func() i32 { values [3]mut i32 = [10, 20, 30] values[bump()] += 5 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) testing.expect_value(t, len(diagnostics.items), 0) call_count := 0 index_address_count := 0 for function in ir_module.functions { if !function.is_main { continue } for instruction in function.instructions { #partial switch instruction.op { case .Call: call_count += 1 case .Index_Address: index_address_count += 1 case: } } } // `bump()` is the lvalue's index. The fix shares one address between the load // and the store, so the side-effecting index runs exactly once and a single // Index_Address is emitted; the buggy double-lowering produced two of each. testing.expect_value(t, call_count, 1) testing.expect_value(t, index_address_count, 1) } @(test) compound_assignment_evaluates_nested_locations_once :: proc(t: ^testing.T) { text := `Box :: struct { value i32 } row func() usize { return 0 } column func() usize { return 1 } pointer_for func(value @mut i32) @mut i32 { return value } main func() i32 { matrix [2]mut [2]mut i32 = [[1, 2], [3, 4]] (matrix[row()])[column()] += 1 boxes [2]mut Box = [Box { value = 5 }, Box { value = 6 }] boxes[row()].value += 1 value i32 = 7 pointer_for(&value)^ += 1 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) testing.expect_value(t, len(diagnostics.items), 0) call_count := 0 call_names: [4]string index_address_count := 0 field_address_count := 0 for function in ir_module.functions { if !function.is_main { continue } for instruction in function.instructions { #partial switch instruction.op { case .Call: function_id := ir.as_function(instruction.target) if call_count < len(call_names) && function_id != ir.INVALID_FUNCTION && int(function_id) < len(hir_module.functions) { call_names[call_count] = symbol.resolve( &symbols, hir_module.functions[function_id].name, ) } call_count += 1 case .Index_Address: index_address_count += 1 case .Field_Address: field_address_count += 1 case: } } } // row(), column(), the second row(), and pointer_for() each run once. The // nested matrix target needs two index addresses; the indexed field needs // one index address and one field address. testing.expect_value(t, call_count, 4) testing.expect_value(t, call_names, [4]string{"row", "column", "row", "pointer_for"}) testing.expect_value(t, index_address_count, 3) testing.expect_value(t, field_address_count, 1) } @(test) compound_assignment_supports_pointer_add_only :: proc(t: ^testing.T) { valid_text := `main func() i32 { values [3]mut i32 = [10, 20, 30] pointer *mut i32 = (&values).ptr pointer += 1 offset usize = 1 pointer += offset return pointer^ } ` source_file := source.Source{path="test.bro", text=valid_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) pointer_add_count := 0 for function in ir_module.functions { if !function.is_main { continue } for instruction in function.instructions { pointer_add_count += 1 if instruction.op == .Pointer_Add else 0 } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, pointer_add_count, 2) invalid_text := `main func() void { values [1]mut i32 = [10] pointer *mut i32 = (&values).ptr pointer -= 1 } ` invalid_source := source.Source{path="invalid.bro", text=invalid_text} invalid_diagnostics := source.init_diagnostics(&invalid_source) defer source.destroy_diagnostics(&invalid_diagnostics) invalid_symbols := symbol.init_table() defer symbol.destroy_table(&invalid_symbols) invalid_stream := lexer.lex(&invalid_source, &invalid_diagnostics, &invalid_symbols) defer delete(invalid_stream.items) invalid_ast := parser.parse(&invalid_stream, &invalid_source, &invalid_diagnostics) defer ast.destroy_module(&invalid_ast) invalid_hir := checker.check(&invalid_ast, &invalid_diagnostics, &invalid_symbols) defer hir.destroy_module(&invalid_hir) found := false for diagnostic in invalid_diagnostics.items { found = found || strings.contains( diagnostic.message, "many-item pointers only support '+=' compound assignment", ) } testing.expect(t, found) } @(test) compound_assignment_preserves_checked_numeric_operations :: proc(t: ^testing.T) { text := `main func() i32 { signed i32 = 24 signed += 6 signed -= 2 signed *= 3 signed /= 4 unsigned u32 = 24 unsigned += 6 unsigned -= 2 unsigned *= 3 unsigned /= 4 real f64 = 24.0 real += 6.0 real -= 2.0 real *= 3.0 real /= 4.0 return signed } ` 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) operation_counts: [hir.Assignment_Op]int for statement_id in hir_module.functions[0].body { statement := hir_module.statements[statement_id] if statement.kind == .Assignment { operation_counts[statement.assignment_op] += 1 } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, operation_counts[.Add], 3) testing.expect_value(t, operation_counts[.Sub], 3) testing.expect_value(t, operation_counts[.Mul], 3) testing.expect_value(t, operation_counts[.Div], 3) add_count := 0 sub_count := 0 mul_count := 0 div_count := 0 for instruction in ir_module.functions[0].instructions { #partial switch instruction.op { case .Add_Checked: add_count += 1 case .Sub_Checked: sub_count += 1 case .Mul_Checked: mul_count += 1 case .Div_Checked: div_count += 1 case: } } testing.expect_value(t, add_count, 3) testing.expect_value(t, sub_count, 3) testing.expect_value(t, mul_count, 3) testing.expect_value(t, div_count, 3) } @(test) compound_assignment_rejects_narrowing_and_mixed_numeric_families :: proc(t: ^testing.T) { text := `main func() void { narrow i8 = 1 wide i32 = 2 narrow += wide signed i32 = 3 unsigned u32 = 4 signed += unsigned } ` 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) found_narrowing := false found_mixed_family := false for diagnostic in diagnostics.items { found_narrowing = found_narrowing || strings.contains(diagnostic.message, "cannot implicitly convert i32 to i8") found_mixed_family = found_mixed_family || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") } testing.expect(t, found_narrowing) testing.expect(t, found_mixed_family) } @(test) compound_assignment_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-compound" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/compound_assignment", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 23) } @(test) binary_arithmetic_rejects_non_numeric_operands :: proc(t: ^testing.T) { text := `main func() i32 { a i32 = 1 b u32 = 2 _ = a / b 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") } testing.expect(t, found) } @(test) compound_assignment_requires_writable_target :: proc(t: ^testing.T) { text := `main func() i32 { x :: 5 x += 1 return x } ` 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "assignment target is not writable") } testing.expect(t, found) } @(test) checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) { text := `main func() i32 { a i32 = 10 b i32 = 3 c i32 = a - b return c / b } ` 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) testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i32")) testing.expect(t, strings.contains(llvm_text, "sdiv i32")) testing.expect(t, strings.contains(llvm_text, "divzero_trap")) testing.expect(t, strings.contains(llvm_text, "divovf_trap")) } @(test) distinct_types_preserve_nominal_identity_and_backing_representation :: proc(t: ^testing.T) { text := `Point :: struct { x i32 y i32 } UserID :: distinct u32 OtherID :: distinct u32 PointID :: distinct Point Bytes :: distinct [2]u8 WrappedID :: distinct UserID static_id UserID :: UserID(42) take func(value UserID) UserID { return value } main func() i32 { id UserID :: UserID(7) copy UserID = take(id) maybe ?UserID = copy pointer @UserID = © point PointID :: PointID(Point { x = 1, y = 2 }) bytes Bytes :: Bytes([3, 4]) wrapped WrappedID :: WrappedID(id) _ = maybe _ = pointer _ = point _ = bytes _ = wrapped 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) user_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "UserID"))) other_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "OtherID"))) point_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "PointID"))) point := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Point"))) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, user_id != other_id) testing.expect(t, user_id != types.U32) testing.expect(t, types.is_distinct(user_id, &ast_module.type_store)) testing.expect_value(t, types.runtime_representation(user_id, &ast_module.type_store), types.U32) testing.expect_value(t, types.runtime_representation(point_id, &ast_module.type_store), point) testing.expect_value(t, types.size(user_id, &ast_module.type_store), types.size(types.U32, &ast_module.type_store)) testing.expect(t, hir_module.globals[0].is_static) testing.expect_value(t, hir_module.globals[0].static_value, i64(42)) testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i32 42")) testing.expect(t, strings.contains(llvm_text, "select i1 true, i32")) retype_count := 0 for function in ir_module.functions { for instruction in function.instructions { retype_count += 1 if instruction.op == .Retype else 0 } } testing.expect_value(t, retype_count, 4) } @(test) distinct_types_reject_implicit_conversions_operators_and_invalid_backings :: proc(t: ^testing.T) { text := `Opaque :: c_struct UserID :: distinct u32 OtherID :: distinct u32 BadInt :: distinct int BadVoid :: distinct void BadFunction :: distinct c_func() void BadOpaque :: distinct Opaque foreign c_func(value UserID) void foreign_pointer c_func(value @UserID) void main func() void { raw u32 = 1 id UserID = raw backing u32 = UserID(2) other OtherID = UserID(3) narrow u8 = 4 _ = UserID(narrow) _ = UserID() _ = UserID(1, 2) left UserID :: UserID(5) right UserID :: UserID(6) _ = left + right _ = left == right } ` 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) invalid_backing_count := 0 implicit_conversion_count := 0 found_exact := false found_arity := false found_arithmetic := false found_comparison := false foreign_signature_count := 0 for diagnostic in diagnostics.items { invalid_backing_count += 1 if strings.contains(diagnostic.message, "requires a concrete runtime backing type") else 0 implicit_conversion_count += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0 found_exact = found_exact || strings.contains(diagnostic.message, "requires an exact u32 value, got u8") found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 argument") found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") found_comparison = found_comparison || strings.contains(diagnostic.message, "comparison requires compatible numeric operands") foreign_signature_count += 1 if strings.contains(diagnostic.message, "requires concrete parameter types") else 0 } testing.expect_value(t, invalid_backing_count, 4) testing.expect(t, implicit_conversion_count >= 3) testing.expect(t, found_exact) testing.expect(t, found_arity) testing.expect(t, found_arithmetic) testing.expect(t, found_comparison) testing.expect_value(t, foreign_signature_count, 2) } @(test) distinct_type_construction_defers_to_callable_names :: proc(t: ^testing.T) { text := `Value :: distinct u32 Value func(value i32) i32 { return value } main func() i32 { return Value(42) } ` 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) found_call := false found_retype := false for expr in hir_module.exprs { found_call = found_call || expr.kind == .Call found_retype = found_retype || expr.kind == .Retype } testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found_call) testing.expect(t, !found_retype) } @(test) distinct_types_compile_and_run_across_packages :: proc(t: ^testing.T) { output := "/tmp/brolang-test-distinct-types" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/distinct_types", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) native_enums_preserve_identity_members_and_integer_representation :: proc(t: ^testing.T) { text := `Animal :: enum { dog cat bird } Nat :: enum(u16) { one = 1 two five = 5 } global Animal :: Animal.dog take func(value Animal) Animal { return value } identity c_func(value Nat) Nat { return value } variadic c_func(marker c_int, ...) c_int main func() i32 { value Animal = .cat values [2]Animal :: [.dog, Animal.bird] number Nat = identity(.two) _ = variadic(0, number) if take(value) == Animal.cat and values[0] != values[1] and number == Nat.two { return 0 } return 1 } ` 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) animal := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Animal"))) nat := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Nat"))) animal_node, animal_ok := types.node(&ast_module.type_store, animal) nat_node, nat_ok := types.node(&ast_module.type_store, nat) animal_members := types.enum_members_for(&ast_module.type_store, animal) nat_members := types.enum_members_for(&ast_module.type_store, nat) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, animal_ok && nat_ok) testing.expect(t, types.is_enum(animal, &ast_module.type_store)) testing.expect_value(t, animal_node.child, types.U16) testing.expect(t, !animal_node.explicit_backing) testing.expect_value(t, nat_node.child, types.U16) testing.expect(t, nat_node.explicit_backing) testing.expect_value(t, len(animal_members), 3) testing.expect_value(t, animal_members[0].value, i128(1)) testing.expect_value(t, animal_members[2].value, i128(3)) testing.expect_value(t, nat_members[0].value, i128(1)) testing.expect_value(t, nat_members[1].value, i128(2)) testing.expect_value(t, nat_members[2].value, i128(5)) testing.expect_value(t, types.runtime_representation(animal, &ast_module.type_store), types.U16) testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(2)) testing.expect(t, hir_module.globals[0].is_static) testing.expect_value(t, hir_module.globals[0].static_value, i64(1)) testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i16 1")) found_promotion := false for function in ir_module.functions { for instruction in function.instructions { found_promotion = found_promotion || instruction.op == .C_Vararg_Promote } } testing.expect(t, found_promotion) } @(test) unbacked_enum_uses_global_u16_backing :: proc(t: ^testing.T) { builder := strings.builder_make() defer strings.builder_destroy(&builder) strings.write_string(&builder, "Large :: enum {\n") for index in 0..<257 { fmt.sbprintf(&builder, "value_%d\n", index) } strings.write_string(&builder, "}\nmain func() void {}\n") source_file := source.Source{path="test.bro", text=strings.to_string(builder)} 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) large := types.find_named(&module.type_store, 0, u32(symbol.intern(&symbols, "Large"))) item, ok := types.node(&module.type_store, large) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, ok) testing.expect_value(t, item.child, types.U16) testing.expect_value(t, len(types.enum_members_for(&module.type_store, large)), 257) } @(test) native_enum_invalid_declarations_and_operations_are_diagnosed :: proc(t: ^testing.T) { text := `Empty :: enum {} Dense :: enum { zero = 0 one } BadBacking :: enum(f32) { value } Duplicate :: enum(u8) { value value } Jumbled :: enum(i8) { second = 2 first = 1 } Overflow :: enum(u8) { value = 256 } Other :: enum { value } foreign c_func(value Dense) void allowed c_func(value Overflow) Overflow main func() void { dense Dense = Other.value _ = Dense.zero + Dense.one _ = Dense.zero < Dense.one _ = Dense.missing _ = .zero _ = dense } ` 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) found_empty := false found_unbacked_value := false found_backing := false found_duplicate := false found_order := false found_overflow := false found_foreign := false found_conversion := false found_arithmetic := false found_comparison := false found_member := false found_context := false for diagnostic in diagnostics.items { found_empty = found_empty || strings.contains(diagnostic.message, "require at least one member") found_unbacked_value = found_unbacked_value || strings.contains(diagnostic.message, "explicit enum values require a backing type") found_backing = found_backing || strings.contains(diagnostic.message, "requires a concrete integer backing type") found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate enum member") found_order = found_order || strings.contains(diagnostic.message, "strictly increasing") found_overflow = found_overflow || strings.contains(diagnostic.message, "does not fit in u8") found_foreign = found_foreign || strings.contains(diagnostic.message, "requires concrete parameter types") found_conversion = found_conversion || strings.contains(diagnostic.message, "cannot implicitly convert") found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") found_comparison = found_comparison || strings.contains(diagnostic.message, "enum values only support") found_member = found_member || strings.contains(diagnostic.message, "unknown enum member") found_context = found_context || strings.contains(diagnostic.message, "requires an enum context") } testing.expect(t, found_empty) testing.expect(t, found_unbacked_value) testing.expect(t, found_backing) testing.expect(t, found_duplicate) testing.expect(t, found_order) testing.expect(t, found_overflow) testing.expect(t, found_foreign) testing.expect(t, found_conversion) testing.expect(t, found_arithmetic) testing.expect(t, found_comparison) testing.expect(t, found_member) testing.expect(t, found_context) } @(test) native_enums_compile_and_run_across_packages :: proc(t: ^testing.T) { output := "/tmp/brolang-test-enums" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/enums", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) translate_c_emits_native_bindings_and_round_trips :: proc(t: ^testing.T) { result := cimport.init_result(context.allocator) // type table: [0]=c_int, [1]=c_ulong, [2]=Record(Pair), // [3]=Function(c_int)->c_int, [4]=Pointer->Function (callback) append(&result.types, cimport.Type{kind = .C_Int, child = cimport.INVALID_TYPE}) append(&result.types, cimport.Type{kind = .C_Ulong, child = cimport.INVALID_TYPE}) append(&result.types, cimport.Type{kind = .Record, record = 0, child = cimport.INVALID_TYPE}) func_params := []cimport.Type_Id{cimport.Type_Id(0)} append(&result.types, cimport.Type{kind = .Function, params = func_params, child = cimport.Type_Id(0)}) append(&result.types, cimport.Type{kind = .Pointer, child = cimport.Type_Id(3)}) // record 0: Pair { left c_int; right c_int } pair_fields: [dynamic]cimport.Field append(&pair_fields, cimport.Field{name = "left", type = cimport.Type_Id(0)}) append(&pair_fields, cimport.Field{name = "right", type = cimport.Type_Id(0)}) append(&result.records, cimport.Record{name = "Pair", fields = pair_fields, kind = .Struct, complete = true}) // record 1: union Choice -> commented (no native spelling) choice_fields: [dynamic]cimport.Field append(&choice_fields, cimport.Field{name = "tag", type = cimport.Type_Id(0)}) append(&result.records, cimport.Record{name = "Choice", fields = choice_fields, kind = .Union, complete = true}) // typedef aliases: a scalar and a callback function pointer append(&result.aliases, cimport.Alias{name = "Size", type = cimport.Type_Id(1)}) append(&result.aliases, cimport.Alias{name = "Mapper", type = cimport.Type_Id(4)}) add_params := []cimport.Type_Id{cimport.Type_Id(0), cimport.Type_Id(0)} add_param_names := []string{"a", "b"} append(&result.functions, cimport.Function{name = "imported_add", params = add_params, param_names = add_param_names, result = cimport.Type_Id(0)}) append(&result.macros, cimport.Macro_Constant{ name = "MAX_LEN", type = cimport.Type_Id(0), value = {kind = .Integer, type = cimport.Type_Id(0), integer = 256}, }) // external variable -> commented (no native spelling) append(&result.variables, cimport.Variable{name = "some_global", type = cimport.Type_Id(0), mutable = true}) result.available = true output := translatec.emit(&result, "test.h") defer delete(output) defer { delete(result.types) delete(result.records) delete(result.aliases) delete(result.functions) delete(result.variables) delete(result.macros) delete(pair_fields) delete(choice_fields) } testing.expect(t, strings.contains(output, "Pair :: c_struct {")) testing.expect(t, strings.contains(output, "\tleft c_int")) testing.expect(t, strings.contains(output, "\tright c_int")) testing.expect(t, strings.contains(output, "Size :: alias c_ulong")) // Function-pointer types carry no parameter names, so the callback renders `_`. testing.expect(t, strings.contains(output, "Mapper :: alias ?*c_func(_ c_int) c_int")) // Real C parameter names are used when present. testing.expect(t, strings.contains(output, "imported_add c_func(a c_int, b c_int) c_int")) testing.expect(t, strings.contains(output, "MAX_LEN c_int :: 256")) testing.expect(t, strings.contains(output, "# unsupported in bindings: C union 'Choice'")) testing.expect(t, strings.contains(output, "# unsupported in bindings: external variable 'some_global'")) // Round-trip: the emitted source must lex + parse with zero diagnostics. // This guards render_type against drift from loader.translate_c_type and // exercises the new `alias` declaration syntax. source_file := source.Source{path = "bindings.bro", text = output} 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) testing.expect_value(t, len(diagnostics.items), 0) } @(test) sink_named_parameters_are_allowed_and_not_duplicates :: proc(t: ^testing.T) { // Generated bindings use `_` for unnamed C params; the parser must accept it and // the checker must not flag repeated `_` as duplicate parameters. text := `foo c_func(_ c_int, _ c_int) c_int main func() void { _ = foo(1, 2) } ` 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) for diagnostic in diagnostics.items { testing.expect(t, !strings.contains(diagnostic.message, "duplicate parameter")) testing.expect(t, !strings.contains(diagnostic.message, "expected parameter name")) } testing.expect_value(t, len(diagnostics.items), 0) } @(test) contextual_inference_resolves_signed_const_chain :: proc(t: ^testing.T) { text := `X :: 1000 Y int :: X Z i32 :: Y main func() void {} ` 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) // The concrete i32 on Z flows backward through Y to the open constant X, so all // three resolve to i32 instead of X/Y staying at the literal's smallest signed type. testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, types.equal(hir_module.globals[0].type, types.I32)) testing.expect(t, types.equal(hir_module.globals[1].type, types.I32)) testing.expect(t, types.equal(hir_module.globals[2].type, types.I32)) } @(test) contextual_inference_open_constants_adopt_unsigned_demand :: proc(t: ^testing.T) { text := `A :: 10 B u16 :: A P :: 10 R u32 :: P N :: 42 main func() void {} ` 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) // An open constant is sign-agnostic until used: it adopts the unsigned family a use // demands (the literal's signed default would block this). Unconstrained N defaults. testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, types.equal(hir_module.globals[0].type, types.U16)) // A testing.expect(t, types.equal(hir_module.globals[1].type, types.U16)) // B testing.expect(t, types.equal(hir_module.globals[2].type, types.U32)) // P testing.expect(t, types.equal(hir_module.globals[3].type, types.U32)) // R testing.expect(t, types.equal(hir_module.globals[4].type, types.I8)) // N } @(test) contextual_inference_rejects_constant_that_does_not_fit_demand :: proc(t: ^testing.T) { text := `BIG :: 100000 C u8 :: BIG main func() void {} ` 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) // 100000 does not fit u8, so the demand is rejected, BIG defaults to i32, and the // genuine mismatch surfaces at the use's boundary coercion. found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8") } testing.expect(t, found) } @(test) contextual_inference_does_not_cross_call_boundaries :: proc(t: ^testing.T) { text := `echo func(p int) int { return p } A :: 10 R u32 :: echo(A) main func() void {} ` 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) // The u32 demand on R must not flow through echo into A (that is L3, deferred). A // stays at its default i8, so the call result fails to coerce to u32. found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "cannot implicitly convert i8 to u32") } testing.expect(t, found) } @(test) contextual_inference_resolves_locals_like_globals :: proc(t: ^testing.T) { text := `take_u16 func(v u16) void {} get func() u16 { c :: 10 return c } main func() void { x :: 1000 y int :: x z i32 :: y a :: 10 b u16 :: a n :: 5 take_u16(n) _ = get() } ` 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) // The same backward propagation works for locals: a constant local adopts the // unsigned/wider type a later use demands (declaration, call argument, or return), // so none of these need an explicit annotation. Without it, i8->u16/u32 would error. testing.expect_value(t, len(diagnostics.items), 0) } @(test) contextual_inference_demand_from_function_body_reaches_global :: proc(t: ^testing.T) { text := `take_u16 func(v u16) void {} G :: 10 main func() void { take_u16(G) } ` 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) // A demand originating inside a function body (passing G to a u16 parameter) flows // back to the open-constant global G, resolving it to u16. testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, types.equal(hir_module.globals[0].type, types.U16)) } @(test) contextual_inference_flows_through_compound_assignment :: proc(t: ^testing.T) { text := `main func() void { s :: 5 v u16 = 0 v += s _ = v } ` 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) // `s` is used only as the RHS of `v += s`. The assignment target's type (u16) is // demanded backward onto `s`, resolving the open constant; without it the compound // assignment would report "arithmetic requires compatible numeric operands". testing.expect_value(t, len(diagnostics.items), 0) } @(test) contextual_inference_resolves_open_global_arithmetic_across_uses :: proc(t: ^testing.T) { text := `take_ci func(v c_int) void {} W :: 800 Z :: 40 STEP :: 5 main func() void { take_ci(W) x int = W - Z take_ci(Z) x += STEP take_ci(x) _ = x } ` 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) // `W - Z` mixes two open-constant globals while `Z`'s c_int use comes only after the // arithmetic, and `STEP` is used only in a compound assignment. With deferred defaulting // an undemanded open global stays typeless during the fixpoint instead of leaking a // provisional i16 default, so W/Z/STEP all resolve to c_int. Previously this reported // "arithmetic requires compatible numeric operands" / "cannot implicitly convert i16 to c_int". testing.expect_value(t, len(diagnostics.items), 0) for global in hir_module.globals { name := symbol.resolve(&symbols, global.name) if name == "W" || name == "Z" || name == "STEP" { testing.expect(t, types.equal(global.type, types.C_INT)) } } } @(test) contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testing.T) { text := `main func() void { big :: 100000 c u8 :: big } ` 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) // 100000 does not fit u8, so big keeps its i32 default and the use errors. found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8") } testing.expect(t, found) } @(test) contextual_inference_flows_through_numeric_arithmetic :: proc(t: ^testing.T) { text := `take_u16 func(v u16) void {} take_f32 func(v f32) void {} G :: 10 H u16 :: G + 2 GF :: 1.5 HF f32 :: GF + 2.5 CG :: 5 CFG :: 1.0 get func() f32 { seed f32 :: 2.0 c :: seed + 3.0 d :: 4.0 + seed return c + d } main func() void { a :: 10 b u16 :: a + 2 x :: 1.5 y f32 :: x + 2.5 z f32 :: 2.5 + x call_i :: 7 call_f :: 1.25 take_u16(call_i + 3) take_f32(call_f + 3.0) take_u16(CG + 1) take_f32(CFG + 1.0) _ = b _ = y _ = z _ = H _ = HF _ = get() } ` 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) testing.expect_value(t, len(diagnostics.items), 0) global_ok := 0 for global in hir_module.globals { name := symbol.resolve(&symbols, global.name) switch name { case "G", "H", "CG": global_ok += 1 if types.equal(global.type, types.U16) else 0 case "GF", "HF", "CFG": global_ok += 1 if types.equal(global.type, types.F32) else 0 } } testing.expect_value(t, global_ok, 6) main_ok := 0 get_ok := false for function in hir_module.functions { name := symbol.resolve(&symbols, function.name) if name == "get" { get_ok = types.equal(function.result, types.F32) for local in function.locals { local_name := symbol.resolve(&symbols, local.name) if local_name == "c" || local_name == "d" { main_ok += 1 if types.equal(local.type, types.F32) else 0 } } } else if name == "main" { for local in function.locals { local_name := symbol.resolve(&symbols, local.name) switch local_name { case "a", "call_i": main_ok += 1 if types.equal(local.type, types.U16) else 0 case "x", "call_f": main_ok += 1 if types.equal(local.type, types.F32) else 0 } } } } testing.expect(t, get_ok) testing.expect_value(t, main_ok, 6) } @(test) contextual_inference_rejects_non_fitting_arithmetic_demand :: proc(t: ^testing.T) { text := `BIG :: 100000 C u8 :: BIG + 1 main func() void { _ = C } ` 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8") } testing.expect(t, found) }