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) parser_marks_only_bang_calls_as_intrinsic :: proc(t: ^testing.T) { text := `main func() void { _ = sizeof ! (i32) _ = sizeof(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) marked := module.exprs[module.statements[module.functions[0].body[0]].expr] ordinary := module.exprs[module.statements[module.functions[0].body[1]].expr] testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, marked.kind, ast.Expr_Kind.Call) testing.expect_value(t, ordinary.kind, ast.Expr_Kind.Call) testing.expect(t, marked.intrinsic) testing.expect(t, !ordinary.intrinsic) } @(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_comptime_value_params :: proc(t: ^testing.T) { text := `make func($N usize, value i32) i32 { return value } 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_dollar := false for tok in stream.items { found_dollar = found_dollar || tok.kind == token.Kind.Dollar } testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, found_dollar) testing.expect_value(t, len(module.functions[0].params), 2) testing.expect(t, module.functions[0].params[0].comptime_value) testing.expect(t, !module.functions[0].params[1].comptime_value) } @(test) parser_accepts_comptime_type_params_and_builtin_type_args :: proc(t: ^testing.T) { text := `id func($T type, value T) T { return value } main func() void { _ = id(i32, 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) module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&module) call := module.exprs[module.statements[module.functions[1].body[0]].expr] type_item, type_ok := types.node(&module.type_store, module.functions[0].params[0].type) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, module.functions[0].params[0].comptime_value) testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].params[0].name), "T") testing.expect(t, type_ok) testing.expect_value(t, symbol.resolve(&symbols, symbol.Id(type_item.name)), "type") testing.expect_value(t, call.kind, ast.Expr_Kind.Call) testing.expect_value(t, module.exprs[call.args[0]].kind, ast.Expr_Kind.Type) } @(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_native_function_pointer_types :: proc(t: ^testing.T) { text := `Error :: enum { bad } take func(callback ?@func(value i32) i32, fallible @func() i32 ! Error) 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) fallible_pointer, fallible_pointer_ok := types.node(&module.type_store, module.functions[0].params[1].type) fallible_function, fallible_function_ok := types.node(&module.type_store, fallible_pointer.child) fallible, fallible_ok := types.node(&module.type_store, fallible_function.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.I32) testing.expect_value(t, len(params), 1) testing.expect(t, params[0].type == types.I32) testing.expect(t, fallible_pointer_ok && fallible_pointer.kind == .Pointer && !fallible_pointer.many) testing.expect(t, fallible_function_ok && fallible_function.kind == .Function && !fallible_function.c_abi) testing.expect(t, fallible_ok && fallible.kind == .Fallible && fallible.child == types.I32) } @(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) } @(test) parser_accepts_comptime_prefix_and_block_expressions :: proc(t: ^testing.T) { text := `sum func(a, b int) int { return a + b } literal :: $32 call :: $sum(1, 2) + 3 block :: ${ yield 4 } 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) literal := module.exprs[module.globals[0].expr] call_add := module.exprs[module.globals[1].expr] call := module.exprs[call_add.left] block := module.exprs[module.globals[2].expr] testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, literal.kind, ast.Expr_Kind.Comptime) testing.expect_value(t, module.exprs[literal.left].kind, ast.Expr_Kind.Integer) testing.expect_value(t, call_add.kind, ast.Expr_Kind.Add) testing.expect_value(t, call.kind, ast.Expr_Kind.Comptime) testing.expect_value(t, module.exprs[call.left].kind, ast.Expr_Kind.Call) testing.expect_value(t, block.kind, ast.Expr_Kind.Comptime) testing.expect_value(t, len(block.body), 1) testing.expect_value(t, module.statements[block.body[0]].kind, ast.Stmt_Kind.Yield) } 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", "--root", ".", "--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, options.project_root, ".") 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_root_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--root", ".", "--root", "other"}) _, 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_root_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) translate_c_helpers_resolve_zig_libc_headers :: proc(t: ^testing.T) { lib_dir, parsed := zig_lib_dir_from_env_output(".{\n .lib_dir = \"/zig/lib\",\n}\n") defer delete(lib_dir) header, resolved := resolve_translate_c_header("native.h", []string{"examples/interop/header/include"}) defer delete(header) testing.expect(t, is_translate_c_command("--translate-c")) testing.expect(t, is_translate_c_command("translate-c")) testing.expect(t, parsed) testing.expect_value(t, lib_dir, "/zig/lib") testing.expect(t, resolved) testing.expect(t, strings.has_suffix(header, "examples/interop/header/include/native.h")) } @(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_source_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.hon")) 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, "--> ") && strings.contains(message, "/b.bro:2:9") && strings.contains(message, "unknown symbol '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_old_function_error := false for diagnostic in diagnostics.items { found_global = found_global || strings.contains(diagnostic.message, "'value' is a global, not a function") found_old_function_error = found_old_function_error || strings.contains(diagnostic.message, "'give' is a function, not a global value") } testing.expect(t, found_global) testing.expect(t, !found_old_function_error) } @(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(_ [*;0]u8) void {} take_mut_sentinel_pointer func(_ [*;0]mut u8) void {} take_pointer func(_ *u8) void {} take_sentinel_slice func(_ [;0]u8) void {} take_mut_sentinel_slice func(_ [;0]mut u8) void {} take_slice func(_ []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) many_item_pointer_slices_compile_and_run :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-pointer-slices" main_path := "/tmp/brolang-test-pointer-slices/main.bro" output := "/tmp/brolang-test-pointer-slices-output" text := `main func() i32 { values [4]mut i32 = [3, 4, 5, 6] pointer *mut i32 :: (&values).ptr const_pointer *i32 :: pointer all []mut i32 :: pointer[..4] middle []mut i32 :: pointer[1..3] readonly []i32 :: const_pointer[..2] if (all.len != 4) return 1 if (middle.len != 2) return 2 all[0] = 10 if (readonly[0] != 10) return 3 if (middle.ptr[0] != 4) return 4 return 0 } ` _ = 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, 0) } @(test) many_item_pointer_slices_require_end_bound :: proc(t: ^testing.T) { text := `main func() void { values [2]i32 = [1, 2] pointer *i32 :: (&values).ptr _ = pointer[..] _ = pointer[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) end_bound_errors := 0 for diagnostic in diagnostics.items { if strings.contains(diagnostic.message, "many-item pointer slicing requires an explicit end bound") { end_bound_errors += 1 } } testing.expect_value(t, end_bound_errors, 2) } @(test) layout_builtins_compile_and_run :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-layout-builtins" main_path := "/tmp/brolang-test-layout-builtins/main.bro" output := "/tmp/brolang-test-layout-builtins-output" text := `Point :: struct { x i32 y u8 } Opaque :: opaque Color :: enum { red blue } UserID :: distinct u32 SIZE_GLOBAL :: sizeof!(i32) needs_usize func(value usize) usize { return value } buffer func($T type) [sizeof!(T)]u8 { data [sizeof!(T)]u8 = undefined return data } main func() i32 { bytes [_]u8 :: buffer(i32) if (needs_usize(SIZE_GLOBAL) != 4) return 1 if (bytes.len != 4) return 2 if (sizeof!([3]u8) != 3) return 3 if (sizeof!([]u8) != 16) return 4 if (alignof!([]u8) != 8) return 5 if (sizeof!(*anyopaque) != 8) return 6 if (sizeof!(?*i32) != 8) return 7 if (sizeof!(*Opaque) != 8) return 8 if (sizeof!(Color) != 2) return 9 if (alignof!(Color) != 2) return 10 if (sizeof!(Point) != 8) return 11 if (alignof!(Point) != 4) return 12 if (sizeof!(UserID) != 4) return 13 if (alignof!(UserID) != 4) return 14 return 0 } ` _ = 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, 0) } @(test) integer_bound_builtins_compile_and_run :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-integer-bounds" main_path := "/tmp/brolang-test-integer-bounds/main.bro" output := "/tmp/brolang-test-integer-bounds-output" text := `MAX_U64 u64 :: maxval!(u64) maximum func($T type) T { return maxval!(T) } main func() i32 { if (minval!(i8) != -128) return 1 if (maxval!(i8) != 127) return 2 if (minval!(u8) != 0) return 3 if (maxval!(u8) != 255) return 4 if (minval!(isize) != -9223372036854775808) return 5 if (maxval!(usize) != 18446744073709551615) return 6 if (MAX_U64 != 18446744073709551615) return 7 if (maximum(u16) != 65535) return 8 if (minval!(c_int) != -2147483648) return 9 if (maxval!(c_ulong) != 18446744073709551615) return 10 return 0 } ` _ = 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, 0) } @(test) integer_bound_builtins_reject_invalid_targets :: proc(t: ^testing.T) { text := `Named :: distinct u8 Choice :: enum { one } main func() void { _ = minval!() _ = maxval!(u8, u16) _ = minval!(1) _ = maxval!(int) _ = maxval!(f32) _ = maxval!(bool) _ = maxval!(Named) _ = maxval!(Choice) } ` 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) bad_arity := 0 bad_type := false bad_target := 0 for diagnostic in diagnostics.items { bad_arity += 1 if strings.contains(diagnostic.message, "expects 1 argument") else 0 bad_type = bad_type || strings.contains(diagnostic.message, "integer bound target must be a type") bad_target += 1 if strings.contains(diagnostic.message, "integer bound target must be a concrete integer type") else 0 } testing.expect_value(t, bad_arity, 2) testing.expect(t, bad_type) testing.expect_value(t, bad_target, 5) } @(test) layout_builtins_reject_unsized_targets :: proc(t: ^testing.T) { text := `Opaque :: opaque Fn :: alias func() void main func() void { _ = sizeof!(void) _ = alignof!(anyopaque) _ = sizeof!(Fn) _ = sizeof!(Opaque) _ = alignof!(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) bad_layout_targets := 0 target_type_error := false for diagnostic in diagnostics.items { bad_layout_targets += 1 if strings.contains(diagnostic.message, "layout target must be a sized runtime value type") else 0 target_type_error = target_type_error || strings.contains(diagnostic.message, "layout target must be a type") } testing.expect_value(t, bad_layout_targets, 4) testing.expect(t, target_type_error) } @(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_bodyless_c_struct_uses_opaque :: proc(t: ^testing.T) { text := `Defined :: c_struct { value c_int } Opaque :: opaque Bodyless :: 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_bodyless := 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_bodyless = found_bodyless || strings.contains(diagnostic.message, "use 'opaque'") 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_bodyless) testing.expect(t, found_bad_layout) testing.expect(t, found_empty) } @(test) opaque_anyopaque_and_ptrcast_compile_and_lower :: proc(t: ^testing.T) { text := `Handle :: opaque take func(_ ?*mut anyopaque) void {} use_handle func(_ ?@mut Handle) void {} main func() void { values [2]mut u8 = [1, 2] raw ?*mut anyopaque = (&values).ptr bytes ?*mut u8 = ptrcast!(u8, raw) take(bytes) if bytes |p| { p[1] = 5 } one u8 = 1 single ?@mut anyopaque = &one typed ?@mut u8 = ptrcast!(u8, single) if typed |p| { p^ = 2 } handle ?@mut Handle = none use_handle(handle) } ` 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) hir_casts := 0 for expr in hir_module.exprs { hir_casts += 1 if expr.kind == .Pointer_Cast else 0 } ir_casts := 0 for function in ir_module.functions { for instruction in function.instructions { ir_casts += 1 if instruction.op == .Pointer_Cast else 0 } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, hir_casts, 2) testing.expect_value(t, ir_casts, 2) } @(test) anyopaque_by_value_and_invalid_ptrcasts_are_rejected :: proc(t: ^testing.T) { text := `Callback :: alias c_func() void main func() void { raw ?*mut anyopaque = none value anyopaque = undefined _ = ptrcast!(void, raw) _ = ptrcast!(anyopaque, raw) _ = ptrcast!(Callback, raw) _ = ptrcast!(u8, 1) _ = ptrcast!(1, raw) } ` 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_by_value := false found_bad_target := false found_bad_operand := false found_target_type := false for diagnostic in diagnostics.items { found_by_value = found_by_value || strings.contains(diagnostic.message, "could not infer a concrete type") found_bad_target = found_bad_target || strings.contains(diagnostic.message, "ptrcast! target must be a sized runtime object type") found_bad_operand = found_bad_operand || strings.contains(diagnostic.message, "ptrcast! operand must be a pointer") found_target_type = found_target_type || strings.contains(diagnostic.message, "ptrcast! target must be a type") } testing.expect(t, found_by_value) testing.expect(t, found_bad_target) testing.expect(t, found_bad_operand) testing.expect(t, found_target_type) } @(test) old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) { text := `main func() void { _ = ptr_cast(1, 1) _ = size_of(1) _ = align_of(1) _ = min_value(1) _ = max_value(1) _ = div_trunc(1, 1) _ = div_floor(1, 1) _ = div_exact(1, 1) _ = div_ceil(1, 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) names := [?]string{ "ptr_cast", "size_of", "align_of", "min_value", "max_value", "div_trunc", "div_floor", "div_exact", "div_ceil", } found := [len(names)]bool{} for diagnostic in diagnostics.items { if !strings.contains(diagnostic.message, "unknown symbol") { continue } for name, index in names { found[index] = found[index] || strings.contains(diagnostic.message, name) } } for value in found { testing.expect(t, value) } } @(test) bare_intrinsic_names_are_available_to_user_functions :: proc(t: ^testing.T) { text := `ptrcast func() i32 { return 1 } sizeof func() i32 { return 2 } alignof func() i32 { return 3 } minval func() i32 { return 4 } maxval func() i32 { return 5 } divtrunc func() i32 { return 6 } divfloor func() i32 { return 7 } divexact func() i32 { return 8 } divceil func() i32 { return 9 } rem func() i32 { return 10 } mod func() i32 { return 11 } main func() i32 { return ptrcast() + sizeof() + alignof() + minval() + maxval() + divtrunc() + divfloor() + divexact() + divceil() + rem() + mod() - 66 } ` directory := "/tmp/brolang-test-user-intrinsic-names" main_path := "/tmp/brolang-test-user-intrinsic-names/main.bro" output := "/tmp/brolang-test-user-intrinsic-names-output" _ = 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)) testing.expect_value(t, compiler_core.compile_package(directory, output), 0) } @(test) intrinsic_call_diagnostics_are_precise :: proc(t: ^testing.T) { text := `main func() void { _ = mystery!() _ = math.ptrcast!() } ` 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_unknown := false found_qualified := false for diagnostic in diagnostics.items { found_unknown = found_unknown || diagnostic.message == "unknown intrinsic 'mystery!'" found_qualified = found_qualified || diagnostic.message == "intrinsic calls must be unqualified" } testing.expect(t, found_unknown) testing.expect(t, found_qualified) } @(test) malformed_intrinsic_calls_have_targeted_parse_diagnostics :: proc(t: ^testing.T) { cases := [?]struct { text, message: string, }{ {`main func() void { _ = sizeof! }`, "expected '(' after intrinsic name"}, {`callback func() void {} main func() void { (callback)!() } `, "intrinsic calls require a direct name"}, } 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 || diagnostic.message == test_case.message } testing.expect(t, found) ast.destroy_module(&module) delete(stream.items) symbol.destroy_table(&symbols) source.destroy_diagnostics(&diagnostics) } } @(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) milestone_33_injects_explicit_io_provider_and_runs_std_io :: 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/programs/io", &sources, &diagnostics, &symbols, project_root_path=".", ) 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(t, loaded) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, hir_module.injected_main != hir.INVALID_FUNCTION) testing.expect(t, hir_module.io_provider != hir.INVALID_FUNCTION) testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main()"), 1) testing.expect_value(t, count_substring_occurrences(llvm_text, "define internal fastcc i32 @bro__p0__main__"), 1) testing.expect_value(t, count_substring_occurrences(llvm_text, "declare i64 @write("), 1) output := "/tmp/brolang-test-io" defer _ = os.remove(output) status := compiler_core.compile_package( "examples/programs/io", output, nil, target.DEFAULT, cimport.Options{}, ".", ) 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) testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, string(stdout), "io-ok 37 {bro}\n" + "bounds=-128/255 -32768/65535 -2147483648/4294967295 -9223372036854775808/18446744073709551615 " + "-9223372036854775808/18446744073709551615 -128/127 -128/255 -32768/65535 -2147483648/4294967295 " + "-9223372036854775808/18446744073709551615 -9223372036854775808/18446744073709551615 0/0 1/1 -1/1\n", ) } @(test) std_io_opens_existing_files_through_the_captured_provider :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-file-io" main_path := "/tmp/brolang-test-file-io/main.bro" data_path := "/tmp/brolang-test-file-io/data" output := "/tmp/brolang-test-file-io/app" seed := "xxxxx" text := `io :: import "@std/io" process :: import "@std/process" missing_fails func(system io.Io) bool { _ = io.open(system, "/tmp/brolang-test-file-io/missing", .read_only) catch |err| { return err == .open_failed } return false } wrong_read_fails func(file io.File) bool { buffer [1]mut u8 = [0] _ = io.read(io.reader(file), buffer[..]) catch |err| { return err == .not_open_for_reading } return false } wrong_write_fails func(file io.File) bool { _ = io.write(io.writer(file), "x") catch |err| { return err == .not_open_for_writing } return false } main func(init process.Init) i32 { if !missing_fails(init.io) { return 1 } read_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .read_only) catch |_| { return 2 } buffer [5]mut u8 = [0, 0, 0, 0, 0] count usize = io.read(io.reader(read_file), buffer[..]) catch |_| { return 4 } if count != 5 or buffer[0] != 'x' or !wrong_write_fails(read_file) { return 5 } io.close(read_file) catch |_| { return 6 } write_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .write_only) catch |_| { return 7 } if !wrong_read_fails(write_file) { return 8 } io.write_all(io.writer(write_file), "bro") catch |_| { return 9 } io.close(write_file) catch |_| { return 10 } read_write_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .read_write) catch |_| { return 11 } count = io.read(io.reader(read_write_file), buffer[..]) catch |_| { return 12 } if count != 5 or buffer[0] != 'b' or buffer[1] != 'r' or buffer[2] != 'o' { return 13 } io.write_all(io.writer(read_write_file), "!") catch |_| { return 14 } io.close(read_write_file) catch |_| { return 15 } return 0 } ` _ = os2.remove_all(directory) defer _ = os2.remove_all(directory) testing.expect(t, os.make_directory(directory) == nil) testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) testing.expect(t, os.write_entire_file(data_path, transmute([]byte)seed)) status := compiler_core.compile_package( directory, output, nil, target.DEFAULT, cimport.Options{}, ".", ) 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) testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, len(stdout), 0) testing.expect_value(t, len(stderr), 0) data, ok := os.read_entire_file(data_path) defer delete(data) testing.expect(t, ok) testing.expect_value(t, string(data), "broxx!") } @(test) milestone_33_rejects_non_io_and_extra_main_parameters :: proc(t: ^testing.T) { cases := [?]string{ "main func(value i32) void {}\n", "main func(left, right i32) void {}\n", "main func($value i32) void {}\n", } for text in cases { source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) symbols := symbol.init_table() stream := lexer.lex(&source_file, &diagnostics, &symbols) ast_module := parser.parse(&stream, &source_file, &diagnostics) hir_module := checker.check(&ast_module, &diagnostics, &symbols) found := false for diagnostic in diagnostics.items { found = found || strings.contains( diagnostic.message, "take no parameters or one @std/process Init", ) } testing.expect(t, found) hir.destroy_module(&hir_module) ast.destroy_module(&ast_module) delete(stream.items) symbol.destroy_table(&symbols) source.destroy_diagnostics(&diagnostics) } } @(test) milestone_37_rejects_direct_io_main_parameter :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-direct-io-main" main_path := "/tmp/brolang-test-direct-io-main/main.bro" text := `io :: import "@std/io" main func(system io.Io) void { _ = system } ` _ = os2.remove_all(directory) defer _ = os2.remove_all(directory) testing.expect(t, os.make_directory(directory) == nil) testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) 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(directory, &sources, &diagnostics, &symbols, project_root_path=".") 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, "take no parameters or one @std/process Init", ) } testing.expect(t, loaded) testing.expect(t, found) } @(test) milestone_37_validates_process_init_and_hidden_system_provider :: proc(t: ^testing.T) { Case :: struct { io_source: string, process_source: string, message: string, } cases := []Case{ { io_source=`Io :: struct { value i32 } system func() Io { return Io {value = 0} } `, process_source=`io :: import "@std/io" Init :: struct { io io.Io } `, message="does not provide the required 'hide system func() Io'", }, { io_source=`Io :: struct { value i32 } hide system func() Io { return Io {value = 0} } `, process_source=`io :: import "@std/io" Init :: struct { io io.Io, extra i32 } `, message="Init must be an auto-layout record containing exactly 'io io.Io'", }, } for test_case, index in cases { root := fmt.tprintf("/tmp/brolang-test-process-schema-%d", index) app := fmt.tprintf("%s/app", root) io_dir := fmt.tprintf("%s/std/io", root) process_dir := fmt.tprintf("%s/std/process", root) main_source: string = `process :: import "@std/process" main func(init process.Init) void { _ = init } ` _ = os2.remove_all(root) defer _ = os2.remove_all(root) testing.expect(t, os2.make_directory_all(app) == nil) testing.expect(t, os2.make_directory_all(io_dir) == nil) testing.expect(t, os2.make_directory_all(process_dir) == nil) testing.expect(t, os.write_entire_file( fmt.tprintf("%s/main.bro", app), transmute([]byte)main_source, )) testing.expect(t, os.write_entire_file( fmt.tprintf("%s/io.bro", io_dir), transmute([]byte)test_case.io_source, )) testing.expect(t, os.write_entire_file( fmt.tprintf("%s/process.bro", process_dir), transmute([]byte)test_case.process_source, )) sources := source.init_store() diagnostics := source.init_store_diagnostics(&sources) symbols := symbol.init_table() ast_module, loaded := loader.load(app, &sources, &diagnostics, &symbols, project_root_path=root) hir_module := checker.check(&ast_module, &diagnostics, &symbols) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, test_case.message) } testing.expect(t, loaded) testing.expect(t, found) hir.destroy_module(&hir_module) ast.destroy_module(&ast_module) symbol.destroy_table(&symbols) source.destroy_diagnostics(&diagnostics) source.destroy_store(&sources) } } @(test) milestone_37_tuples_reflection_expand_for_and_debug_print_compile_and_run :: 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/programs/tuples", &sources, &diagnostics, &symbols, project_root_path=".", ) 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(t, loaded) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, !strings.contains(llvm_text, "FormatToken")) testing.expect(t, !strings.contains(llvm_text, "parse_format")) testing.expect(t, !strings.contains(llvm_text, "format_field_name")) testing.expect(t, !strings.contains(llvm_text, "FieldInfo")) testing.expect(t, !strings.contains(llvm_text, "RecordInfo")) output := "/tmp/brolang-test-tuples" defer _ = os.remove(output) status := compiler_core.compile_package( "examples/programs/tuples", output, nil, target.DEFAULT, cimport.Options{}, ".", ) 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) testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, string(stdout), "") testing.expect_value(t, string(stderr), "hello!\ntuple=40/bro, limits=-9223372036854775808/18446744073709551615") } @(test) expanded_matches_and_tag_intrinsics_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-expand" defer _ = os.remove(output) status := compiler_core.compile_package( "examples/programs/expand", output, nil, target.DEFAULT, cimport.Options{}, ".", ) 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) testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, string(stdout), "") testing.expect_value(t, string(stderr), "") } @(test) expanded_match_and_tag_diagnostics :: proc(t: ^testing.T) { text := `E :: enum { a, b } Plain :: union { value i32 } main func() void { inline for {1} |value| { _ = value } n i32 = 1 match n { expand |value|: _ = value } e E = .a match e { expand |value, tag|: _ = value } match e { .a, .b: {} expand |value|: _ = value } match e { expand |value|: _ = value .b: {} } match e { else: {} expand |value|: _ = value } u Plain = Plain{value = 1} _ = tag!(u) _ = tagname!(e) match e { expand ||: {} } match e { expand |a, b, c|: {} } } ` source_file := source.Source{path="expand_diagnostics.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_old := false found_subject := false found_captures := false found_redundant := false found_after := false found_missing := false found_many := false found_tag := false found_tagname := false for diagnostic in diagnostics.items { message := diagnostic.message found_old = found_old || strings.contains(message, "'inline for' was renamed to 'expand for'") found_subject = found_subject || strings.contains(message, "'expand' requires an enum or tagged-union") found_captures = found_captures || strings.contains(message, "requires exactly one capture") found_redundant = found_redundant || strings.contains(message, "redundant 'expand'") found_after = found_after || strings.contains(message, "arms after 'expand' are unreachable") || strings.contains(message, "arms after 'else' are unreachable") found_missing = found_missing || strings.contains(message, "expected an expand value capture") found_many = found_many || strings.contains(message, "at most two captures") found_tag = found_tag || strings.contains(message, "tag! requires a tagged-union value") found_tagname = found_tagname || strings.contains(message, "tagname! requires a comptime-known enum value") } testing.expect(t, found_old) testing.expect(t, found_subject) testing.expect(t, found_captures) testing.expect(t, found_redundant) testing.expect(t, found_after) testing.expect(t, found_missing) testing.expect(t, found_many) testing.expect(t, found_tag) testing.expect(t, found_tagname) } @(test) milestone_37_format_errors_are_reported_at_comptime :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-format-errors" main_path := "/tmp/brolang-test-format-errors/main.bro" text := `io :: import "@std/io" process :: import "@std/process" main func(init process.Init) void { writer io.Writer :: io.stdout(init.io) stored :: typeinfo!(i32) _ = stored io.print(writer, "", 1) catch |_| {} io.print(writer, "{s}", {1,}) catch |_| {} io.print(writer, "{d}", {"bro",}) catch |_| {} io.print(writer, "{d}{d}", {1,}) catch |_| {} io.print(writer, "{d}", {1, 2}) catch |_| {} io.print(writer, "{q}", {}) catch |_| {} io.print(writer, "{b}", {1.5,}) catch |_| {} io.print(writer, "{e}", {1,}) catch |_| {} io.print(writer, "{c}", {i16(65),}) catch |_| {} io.print(writer, "}", {}) catch |_| {} } ` _ = os2.remove_all(directory) defer _ = os2.remove_all(directory) testing.expect(t, os.make_directory(directory) == nil) testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) 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(directory, &sources, &diagnostics, &symbols, project_root_path=".") defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) found := [10]bool{} for diagnostic in diagnostics.items { message := diagnostic.message found[0] = found[0] || strings.contains(message, "arguments must be a tuple") found[1] = found[1] || strings.contains(message, "cannot implicitly convert i8 to []u8") found[2] = found[2] || strings.contains(message, "'{d}' requires an integer or float") found[3] = found[3] || strings.contains(message, "argument count does not match") found[4] = found[4] || strings.contains(message, "unknown specifier") found[5] = found[5] || strings.contains(message, "unmatched '}'") found[6] = found[6] || strings.contains(message, "compile-time-only metadata") found[7] = found[7] || strings.contains(message, "integer format requires an integer argument") found[8] = found[8] || strings.contains(message, "float format requires a float argument") found[9] = found[9] || strings.contains(message, "'{c}' requires an unsigned integer that fits in u8") } testing.expect(t, loaded) for present in found { testing.expect(t, present) } } @(test) milestone_39_stable_values_and_richer_formatting_compile_and_run :: proc(t: ^testing.T) { stable_names: [dynamic]string defer { for name in stable_names { delete(name) } delete(stable_names) } for pass := 0; pass < 2; pass += 1 { sources := source.init_store() diagnostics := source.init_store_diagnostics(&sources) symbols := symbol.init_table() ast_module, loaded := loader.load( "examples/programs/milestone_39", &sources, &diagnostics, &symbols, project_root_path=".", ) hir_module := checker.check(&ast_module, &diagnostics, &symbols) ir_module := lower.lower(&hir_module) llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) score_count := 0 for function in hir_module.functions { if strings.contains(function.link_name, "bro__p0__score__ca") { if pass == 0 { append(&stable_names, strings.clone(function.link_name)) } else { testing.expect_value(t, function.link_name, stable_names[score_count]) } score_count += 1 } } testing.expect(t, loaded) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, score_count, 2) testing.expect_value(t, count_substring_occurrences(llvm_text, "define internal fastcc i32 @bro__p0__read_carrier__"), 1) testing.expect(t, !strings.contains(llvm_text, "FormatToken")) testing.expect(t, !strings.contains(llvm_text, "parse_format")) testing.expect(t, !strings.contains(llvm_text, "FieldInfo")) testing.expect(t, !strings.contains(llvm_text, "EnumInfo")) delete(llvm_text) ir.destroy_module(&ir_module) hir.destroy_module(&hir_module) ast.destroy_module(&ast_module) symbol.destroy_table(&symbols) source.destroy_diagnostics(&diagnostics) source.destroy_store(&sources) } output := "/tmp/brolang-test-milestone-39" defer _ = os.remove(output) status := compiler_core.compile_package( "examples/programs/milestone_39", output, nil, target.DEFAULT, cimport.Options{}, ".", ) 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) testing.expect_value(t, state.exit_code, 0) testing.expect_value( t, string(stdout), "true -42 1.5 .running bro 2.5 1010 12 ff FF A 1.5000000000000000e+00 {} -9223372036854775808 0 inf nan 1.50000000e+00\n", ) testing.expect_value(t, string(stderr), "debug=.idle 2a\n") } @(test) milestone_39_rejects_values_without_stable_identity :: proc(t: ^testing.T) { text := `BadUnion :: union { number i32, flag bool } Config :: struct { value i32 } identity func() i32 { return 1 } reject_pointer func($value @i32) void {} reject_function func($value @func() i32) void {} reject_slice func($value []i32) void {} reject_range func($value range) void {} reject_union func($value BadUnion) void {} reject_undefined func($value Config) void {} stored i32 :: 1 items [2]i32 :: [1, 2] main func() void { reject_pointer(&stored) reject_function(identity) reject_slice(items[..]) reject_range(0..3) reject_union(BadUnion {number = 1}) reject_undefined(Config {value = 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) found := 0 for diagnostic in diagnostics.items { if strings.contains(diagnostic.message, "comptime argument has no stable comptime identity") { found += 1 } } testing.expect(t, found >= 6) } @(test) milestone_37_expand_loop_control_must_be_statically_resolvable :: proc(t: ^testing.T) { text := `main func() void { total i32 = 0 expand for {1, 2} |value| { if total == 0 { break } total += 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) found := false for diagnostic in diagnostics.items { found = found || strings.contains( diagnostic.message, "break or continue targeting an expand loop must be compile-time-resolvable", ) } testing.expect(t, found) } @(test) milestone_37_comptime_undefined_aggregates_support_full_initialization :: proc(t: ^testing.T) { text := `Token :: struct { text []u8 count usize } Partial :: struct { initialized i32, text []u8 } make_tokens func() [2]mut Token { tokens [2]mut Token = undefined tokens[0] = Token {text = "a", count = 1} tokens[1].text = "bro" tokens[1].count = 3 return tokens } read_initialized_sibling func() i32 { value Partial = undefined value.initialized = 42 return value.initialized } answer :: $read_initialized_sibling() main func() i32 { total usize = 0 expand for make_tokens() |token| { total += token.text.len + token.count } if answer != 42 or total != 8 { return 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) testing.expect_value(t, len(diagnostics.items), 0) } @(test) milestone_37_comptime_undefined_values_cannot_be_observed :: proc(t: ^testing.T) { text := `Bad :: struct { value i32, text []u8 } read_scalar func() i32 { value i32 = undefined return value } return_partial func() Bad { value Bad = undefined value.value = 1 return value } take_bad func(value Bad) i32 { return value.value } pass_partial func() i32 { value Bad = undefined value.value = 1 return take_bad(value) } bad_scalar :: $read_scalar() bad_record :: $return_partial() bad_argument :: $pass_partial() 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_read := false found_return := false found_pass := false for diagnostic in diagnostics.items { found_read = found_read || strings.contains(diagnostic.message, "cannot read an undefined value at comptime") found_return = found_return || strings.contains(diagnostic.message, "comptime function returned an undefined value") found_pass = found_pass || strings.contains(diagnostic.message, "cannot pass an undefined value at comptime") } testing.expect(t, found_read) testing.expect(t, found_return) testing.expect(t, found_pass) } @(test) milestone_37_expand_expansions_keep_distinct_call_resolutions :: proc(t: ^testing.T) { text := `identity func($T type, value T) T { return value } main func() i32 { total i64 = 0 expand for {{i8(1), i16(2)}, {i32(3), i64(4)}} |row| { expand for row |value| { total += i64(identity(value)) } } return i32(total - 10) } ` 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) identity_symbol := symbol.intern(&symbols, "identity") specializations := 0 for function in hir_module.functions { specializations += 1 if function.name == identity_symbol else 0 } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, specializations, 4) } @(test) milestone_37_expand_control_prunes_inference_after_static_exit :: proc(t: ^testing.T) { text := `take_i8 func(value i8) void { _ = value } main func() void { expand for {i8(1), "skip"} |value, index| { if index == 1 { continue } take_i8(value) } expand for {i8(1), "stop"} |value, index| { if index == 1 { break } take_i8(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) testing.expect_value(t, len(diagnostics.items), 0) } @(test) milestone_37_expand_match_specialization_prunes_unselected_arms :: proc(t: ^testing.T) { text := `Kind :: enum { integer, string, stop } IntToken :: struct { kind Kind, value i8 } StringToken :: struct { kind Kind, value []u8 } StopToken :: struct { kind Kind, value bool } take_i8 func(value i8) void { _ = value } main func() void { expand for { IntToken {kind = .integer, value = 1}, StringToken {kind = .string, value = "ok"}, StopToken {kind = .stop, value = false}, } |token| { match token.kind { .integer: take_i8(token.value) .string: { _ = token.value.len continue } .stop: break } } expand for {i8(2), "skip", "stop"} |value, index| { match index { 0: {} 1..=1, 7: continue else: break } take_i8(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) testing.expect_value(t, len(diagnostics.items), 0) } @(test) milestone_33_rejects_an_incompatible_runtime_write_declaration :: proc(t: ^testing.T) { text := `write c_func(_ c_int, _ c_int, _ c_ulong) c_long 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 := false for diagnostic in diagnostics.items { found = found || strings.contains( diagnostic.message, "external C function 'write' conflicts with the compiler runtime declaration", ) } testing.expect(t, found) } @(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 _ = local _ = 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) bare_returns_and_strict_yields :: proc(t: ^testing.T) { text := `Failure :: enum { bad } noop func() void {} newline_return func() void { return } inline_return func() void { return } fallible_return func() void ! Failure { return } split_return func() i32 { return 1 } old_return func() void { return _ } missing_yield func() i32 { value :: { yield 1 } return value } missing_labeled_yield func() i32 { value :: block: { yield :block } return value } void_yield func() i32 { value :: { yield noop() } return value } sink_yield func() i32 { value :: { yield _ } return value } void_context func() void { fallible_return() catch |_| { yield 1 } } bad_comptime :: ${ yield noop() } main func() void { newline_return() inline_return() fallible_return() catch |_| {} _ = split_return() old_return() _ = missing_yield() _ = missing_labeled_yield() _ = void_yield() _ = sink_yield() void_context() } ` 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, ast_module.statements[ast_module.functions[1].body[0]].expr, ast.INVALID_EXPR) testing.expect_value(t, ast_module.statements[ast_module.functions[2].body[0]].expr, ast.INVALID_EXPR) testing.expect_value(t, ast_module.statements[ast_module.functions[3].body[0]].expr, ast.INVALID_EXPR) testing.expect_value(t, len(ast_module.functions[4].body), 2) missing_value := false non_void_function := false void_yields := 0 sink_read := false void_context := false for diagnostic in diagnostics.items { missing_value = missing_value || strings.contains(diagnostic.message, "'yield' must produce a value") non_void_function = non_void_function || strings.contains(diagnostic.message, "non-void function must return a value") if strings.contains(diagnostic.message, "'yield' expression must produce a non-void value") { void_yields += 1 } sink_read = sink_read || strings.contains(diagnostic.message, "'_' is a write-only sink and cannot be read") void_context = void_context || strings.contains(diagnostic.message, "void value context must fall through instead of yielding") } testing.expect(t, missing_value) testing.expect(t, non_void_function) testing.expect(t, void_yields >= 2) testing.expect(t, sink_read) testing.expect(t, void_context) } @(test) unused_locals_and_params_warn_without_traps :: proc(t: ^testing.T) { text := `warn_only func(value i32, unused i32) i32 { local i32 = 1 write_only i32 = 2 write_only = 3 consumed i32 = value _ = consumed return value } main func() void { _ = warn_only(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) found_unused_param := false found_unused_local := false found_write_only := false warning_count := 0 error_count := 0 for diagnostic in diagnostics.items { if diagnostic.severity == source.Severity.Warning { warning_count += 1 } else { error_count += 1 } found_unused_param = found_unused_param || strings.contains(diagnostic.message, "unused parameter 'unused'") found_unused_local = found_unused_local || strings.contains(diagnostic.message, "unused local 'local'") found_write_only = found_write_only || strings.contains(diagnostic.message, "unused local 'write_only'") testing.expect(t, !strings.contains(diagnostic.message, "unused local 'consumed'")) testing.expect(t, !strings.contains(diagnostic.message, "unused parameter 'value'")) } testing.expect_value(t, warning_count, 3) testing.expect_value(t, error_count, 0) testing.expect(t, found_unused_param) testing.expect(t, found_unused_local) testing.expect(t, found_write_only) found_function := false for function in hir_module.functions { if symbol.resolve(&symbols, function.name) != "warn_only" { continue } found_function = true testing.expect(t, !function.problematic) for stmt_id in function.body { testing.expect(t, hir_module.statements[stmt_id].kind != hir.Stmt_Kind.Trap) } } testing.expect(t, found_function) } @(test) generic_parameter_usage_is_source_based :: proc(t: ^testing.T) { text := `choose func($N usize, used, unused i32) i32 { if N > 0 { return used } return 0 } main func() void { _ = choose(0, 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) testing.expect_value(t, len(diagnostics.items), 1) testing.expect_value(t, diagnostics.items[0].severity, source.Severity.Warning) testing.expect(t, strings.contains(diagnostics.items[0].message, "unused parameter 'unused'")) testing.expect(t, !strings.contains(diagnostics.items[0].message, "unused parameter 'used'")) } @(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) testing.expect(t, types.equal(hir_module.globals[0].type, types.I32)) testing.expect(t, types.equal(hir_module.globals[1].type, types.I8)) } @(test) comptime_value_params_specialize_by_value_and_omit_runtime_args :: proc(t: ^testing.T) { text := `make_array func($N usize) [N]u8 { data [N]u8 = undefined return data } main func() void { four [_]u8 :: make_array(4) eight [_]u8 :: make_array(8) again [_]u8 :: make_array(4) literal [_]u8 :: [1, 2, 3, 4] _ = four _ = eight _ = again _ = literal } ` 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) make_specs := 0 for function in hir_module.functions { if symbol.resolve(&symbols, function.name) == "make_array" { make_specs += 1 testing.expect_value(t, len(function.params), 0) } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, make_specs, 2) testing.expect(t, strings.contains(llvm_text, "@bro__p0__make_array__cv4")) testing.expect(t, strings.contains(llvm_text, "@bro__p0__make_array__cv8")) } @(test) comptime_value_params_diagnose_invalid_uses :: proc(t: ^testing.T) { text := `make func($N usize) i32 { return N } tiny func($N u8) i32 { return N } good_bool func($T bool) void {} bad_use func($N usize) void { N = 1 _ = &N } main func() void { good_bool(true) x usize = 4 _ = make(x) _ = make() _ = make(1, 2) _ = make(-1) _ = tiny(300) bad_use(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) found_runtime_arg := false found_missing := false found_extra := false found_negative := false found_range := false found_assignment := false found_address := false for diagnostic in diagnostics.items { message := diagnostic.message found_runtime_arg = found_runtime_arg || strings.contains(message, "must be a compile-time integer expression") found_missing = found_missing || strings.contains(message, "cannot infer comptime parameter 'N'") found_extra = found_extra || strings.contains(message, "has no unique complete argument mapping") found_negative = found_negative || strings.contains(message, "integer constant -1 does not fit in usize") found_range = found_range || strings.contains(message, "integer constant 300 does not fit in u8") found_assignment = found_assignment || strings.contains(message, "cannot assign comptime parameter 'N'") found_address = found_address || strings.contains(message, "'&' requires an addressable location") } testing.expect(t, found_runtime_arg) testing.expect(t, found_missing) testing.expect(t, found_extra) testing.expect(t, found_negative) testing.expect(t, found_range) testing.expect(t, found_assignment) testing.expect(t, found_address) } @(test) inferred_comptime_params_diagnose_ambiguous_calls :: proc(t: ^testing.T) { text := `Ignored func($T type) type { return i32 } Box func($T type) type { return struct { value T } } BoxAlias func($T type) type { return Box(T) } conflict func($T type, left, right T) T { return left } unknown func($T type) T { value T = undefined; return value } partial func($T type, $N usize, value T) T { return value } use_ignored func($T type, value Ignored(T)) i32 { return value } use_alias func($T type, value BoxAlias(T)) T { return value.value } mapping_fail func($A usize, value i32, $B usize) void {} main func() void { a i32 :: 1 b u32 :: 2 _ = conflict(a, b) _ = unknown() _ = partial(i32, a) _ = use_ignored(a) box Box(i32) :: Box(i32) { value = 1 } _ = use_alias(box) mapping_fail(true, 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) found_conflict := false found_unknown := false found_partial := false found_candidate_failures := false unrecoverable := 0 for diagnostic in diagnostics.items { message := diagnostic.message found_conflict = found_conflict || strings.contains(message, "conflicting inference for comptime parameter 'T': i32 and u32") found_unknown = found_unknown || strings.contains(message, "cannot infer comptime parameter 'T'") found_partial = found_partial || strings.contains(message, "cannot infer comptime parameter 'N'") found_candidate_failures = found_candidate_failures || strings.contains(message, "candidate 1: cannot infer comptime parameter 'B'") && strings.contains(message, "candidate 2: cannot infer comptime parameter 'A'") if strings.contains(message, "cannot infer comptime parameter 'T'") { unrecoverable += 1 } } testing.expect(t, found_conflict) testing.expect(t, found_unknown) testing.expect(t, found_partial) testing.expect(t, found_candidate_failures) testing.expect(t, unrecoverable >= 3) } @(test) comptime_params_may_be_interleaved_and_are_erased_from_the_abi :: proc(t: ^testing.T) { text := `valid func($T type, $N usize, value T) [N]T { result [N]T = undefined _ = value return result } runtime_first func(value T, $T type, $N usize) T { return value } split func($T type, value T, $N usize) [N]T { result [N]T = undefined _ = value return result } from_result func($T type) T { value T = undefined return value } choose_mapping func($A usize, value i32, $B usize) [A]u8 { result [A]u8 = undefined _ = value _ = B return result } main func() void { _ = runtime_first(42, i32, 4) _ = runtime_first(42, _, 4) _ = split(i32, 42, 4) _ = split(42, 4) value i32 = from_result() chosen [1]u8 :: choose_mapping(7, 2) _ = value _ = chosen } ` 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 function in hir_module.functions { name := symbol.resolve(&symbols, function.name) if name == "runtime_first" || name == "split" { testing.expect_value(t, len(function.params), 1) } else if name == "from_result" { testing.expect_value(t, len(function.params), 0) } else if name == "choose_mapping" { testing.expect_value(t, len(function.params), 1) } } testing.expect_value(t, len(diagnostics.items), 0) } @(test) comptime_type_params_specialize_by_type_and_omit_runtime_args :: proc(t: ^testing.T) { text := `Point :: struct { x i32 } id func($T type, value T) T { return value } zero func($T type) T { value T = undefined return value } buffer func($T type, $N usize, _ T) [N]T { data [N]T = undefined return data } main func() void { a i32 :: 42 b u8 :: 7 p Point :: Point { x = 9 } _ = id(i32, a) _ = id(u8, b) _ = id(Point, p) _ = zero(i32) _ = buffer(u8, 4, 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) id_specs := 0 zero_specs := 0 buffer_specs := 0 for function in hir_module.functions { name := symbol.resolve(&symbols, function.name) if name == "id" { id_specs += 1 testing.expect_value(t, len(function.params), 1) } else if name == "zero" { zero_specs += 1 testing.expect_value(t, len(function.params), 0) } else if name == "buffer" { buffer_specs += 1 testing.expect_value(t, len(function.params), 1) } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, id_specs, 3) testing.expect_value(t, zero_specs, 1) testing.expect_value(t, buffer_specs, 1) testing.expect(t, strings.contains(llvm_text, "@bro__p0__id__i32__cti32")) testing.expect(t, strings.contains(llvm_text, "@bro__p0__zero__cti32")) testing.expect(t, strings.contains(llvm_text, "@bro__p0__buffer__u8__ctu8__cv4")) } @(test) comptime_type_params_diagnose_invalid_uses :: proc(t: ^testing.T) { text := `id func($T type, value T) T { return value } bad_c c_func($T type) void bad_value func($T type) void { _ = T } bad_assign func($T type) void { T = 1 } main func() void { x i32 = 1 _ = id(x, x) bad_value(i32) bad_assign(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) 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_arg := false found_c_func := false found_value := false found_assign := false for diagnostic in diagnostics.items { message := diagnostic.message found_arg = found_arg || strings.contains(message, "argument for comptime type parameter 'T' must be a type") found_c_func = found_c_func || strings.contains(message, "comptime parameters require 'func', not 'c_func'") found_value = found_value || strings.contains(message, "type parameter 'T' is not a runtime value") found_assign = found_assign || strings.contains(message, "cannot assign comptime parameter 'T'") } testing.expect(t, found_arg) testing.expect(t, found_c_func) testing.expect(t, found_value) testing.expect(t, found_assign) } @(test) comptime_expression_forces_integer_evaluation :: proc(t: ^testing.T) { text := `sum func(a, b int) int { return a + b } max func(a, b int) int { if a > b { return a } return b } nested func(value int) int { two :: 2 return sum(value, two) } make_array func($N usize) [N]u8 { data [N]u8 = undefined return data } forced :: $sum(1, 2) main func() i32 { value i32 :: $sum(20, 22) choice i32 :: $max(9, 3) blocked i32 :: ${ local :: 5 yield sum(local, 6) } bytes [_]u8 :: make_array($nested(2)) if forced != 3 { return 1 } if value != 42 { return 2 } if choice != 9 { return 3 } if blocked != 11 { return 4 } if bytes.len != 4 { return 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) 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.g.0 = internal constant i8 3")) } @(test) comptime_expression_diagnoses_runtime_only_and_quota :: proc(t: ^testing.T) { text := `native c_func() i32 spin func() i32 { while true { } return 0 } missing func() i32 { if true { } } GLOBAL :: 1 main func() void { runtime i32 = 1 _ = $runtime _ = $native() _ = ${ callback :: native yield callback() } _ = $&GLOBAL _ = $spin() _ = $missing() _ = ${ 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_runtime := false runtime_only_count := 0 found_pointer := false found_quota := false found_missing := false found_yield := false for diagnostic in diagnostics.items { message := diagnostic.message found_runtime = found_runtime || strings.contains(message, "unresolved comptime value 'runtime'") runtime_only_count += 1 if strings.contains(message, "runtime-only") else 0 found_pointer = found_pointer || strings.contains(message, "comptime storage pointers and slices cannot materialize as runtime memory") found_quota = found_quota || strings.contains(message, "comptime evaluation exceeded the step quota") found_missing = found_missing || strings.contains(message, "did not return a value") found_yield = found_yield || strings.contains(message, "comptime block must yield a value") } testing.expect(t, found_runtime) testing.expect(t, runtime_only_count >= 2) testing.expect(t, found_pointer) testing.expect(t, found_quota) testing.expect(t, found_missing) testing.expect(t, found_yield) } @(test) native_function_pointer_type_restrictions_are_diagnosed :: proc(t: ^testing.T) { text := `main func() void { callback @func(...) void = 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) found_variadic := false for diagnostic in diagnostics.items { found_variadic = found_variadic || strings.contains(diagnostic.message, "native function pointer types do not support variadic parameters") } testing.expect(t, found_variadic) } @(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) unused_local_warnings_return_status_one_but_do_not_trap :: proc(t: ^testing.T) { output := "/tmp/brolang-test-unused-locals" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/unused_locals", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 7) } @(test) build_command_is_recognized :: proc(t: ^testing.T) { testing.expect(t, is_build_command("build")) testing.expect(t, !is_build_command("translate-c")) testing.expect(t, !is_build_command("--build")) } @(test) version_command_is_recognized :: proc(t: ^testing.T) { testing.expect(t, is_version_command("version")) testing.expect(t, !is_version_command("--version")) testing.expect(t, len(BROLANG_VERSION) > 0) } @(test) build_subcommand_compiles_and_runs :: proc(t: ^testing.T) { output := "examples/build/hello/build/hello" defer _ = os2.remove_all("examples/build/hello/build") status := compiler_core.run_build("examples/build/hello") testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) status = compiler_core.run_build("examples/build/hello") testing.expect_value(t, status, 0) testing.expect(t, os.exists(output)) } @(test) build_subcommand_links_c_source_via_list_field :: proc(t: ^testing.T) { output := "examples/build/manual/build/manual_build" defer _ = os2.remove_all("examples/build/manual/build") status := compiler_core.run_build("examples/build/manual") testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 42) } @(test) new_project_creates_layout_and_builds :: proc(t: ^testing.T) { root := "/tmp/brolang-test-new-project" output := "/tmp/brolang-test-new-project/build/brolang-test-new-project" _ = os2.remove_all(root) defer _ = os2.remove_all(root) testing.expect_value(t, run_new_project(root), 0) testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/source")) testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/std")) testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/ffi")) testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/vendor")) testing.expect(t, os.exists("/tmp/brolang-test-new-project/build.bro")) testing.expect(t, os.exists("/tmp/brolang-test-new-project/source/main.bro")) status := compiler_core.run_build(root) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) init_project_creates_missing_layout_and_preserves_existing_files :: proc(t: ^testing.T) { root := "/tmp/brolang-test-init-project" output := "/tmp/brolang-test-init-project/build/brolang-test-init-project" _ = os2.remove_all(root) defer _ = os2.remove_all(root) testing.expect(t, os2.make_directory_all(root) == nil) testing.expect(t, init_project(root)) testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/source")) testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/std")) testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/ffi")) testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/vendor")) status := compiler_core.run_build(root) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) custom_build := "custom build" custom_main := "custom main" testing.expect(t, os.write_entire_file("/tmp/brolang-test-init-project/build.bro", transmute([]byte)custom_build)) testing.expect(t, os.write_entire_file("/tmp/brolang-test-init-project/source/main.bro", transmute([]byte)custom_main)) testing.expect(t, init_project(root)) build_data, build_ok := os.read_entire_file("/tmp/brolang-test-init-project/build.bro") defer delete(build_data) main_data, main_ok := os.read_entire_file("/tmp/brolang-test-init-project/source/main.bro") defer delete(main_data) testing.expect(t, build_ok && string(build_data) == custom_build) testing.expect(t, main_ok && string(main_data) == custom_main) } @(test) build_root_search_finds_nearest_parent_build_file :: proc(t: ^testing.T) { root := "/tmp/brolang-test-build-search" nested := "/tmp/brolang-test-build-search/source/nested" _ = os2.remove_all(root) defer _ = os2.remove_all(root) testing.expect(t, os2.make_directory_all(nested) == nil) build_text := "# root\n" testing.expect(t, os.write_entire_file("/tmp/brolang-test-build-search/build.bro", transmute([]byte)build_text)) found, ok := compiler_core.find_build_root_from(nested) defer delete(found) testing.expect(t, ok) testing.expect(t, strings.has_suffix(found, "/brolang-test-build-search")) } @(test) build_subcommand_discovered_root_writes_to_root_build_dir :: proc(t: ^testing.T) { root := "/tmp/brolang-test-build-nested" nested := "/tmp/brolang-test-build-nested/source/nested" output := "/tmp/brolang-test-build-nested/build/brolang-test-build-nested" _ = os2.remove_all(root) defer _ = os2.remove_all(root) testing.expect(t, init_project(root)) testing.expect(t, os2.make_directory_all(nested) == nil) found, ok := compiler_core.find_build_root_from(nested) defer delete(found) testing.expect(t, ok) status := compiler_core.run_build(found) testing.expect_value(t, status, 0) testing.expect(t, os.exists(output)) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) build_subcommand_rejects_path_like_output_name :: proc(t: ^testing.T) { root := "/tmp/brolang-test-build-invalid-name" _ = os2.remove_all(root) defer _ = os2.remove_all(root) testing.expect(t, init_project(root)) text := `b :: import "@std/build" config :: b.BuildConfig{ name = "bad/name", source = "source", libraries = &[], lib_paths = &[], includes = &[], defines = &[], links = &[], } ` testing.expect(t, os.write_entire_file("/tmp/brolang-test-build-invalid-name/build.bro", transmute([]byte)text)) status := compiler_core.run_build(root) testing.expect_value(t, status, 2) testing.expect(t, !os.exists("/tmp/brolang-test-build-invalid-name/build/bad/name")) } @(test) project_root_imports_resolve_under_passed_root :: 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/build/raylib/source", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, "examples/build/raylib", ) defer ast.destroy_module(&ast_module) testing.expect(t, loaded) found_vendor := false for pkg in ast_module.packages { found_vendor = found_vendor || strings.has_suffix(pkg.path, "/examples/build/raylib/vendor/raylib") } testing.expect(t, found_vendor) } @(test) direct_compile_defaults_project_root_to_input_package_and_allows_override :: proc(t: ^testing.T) { root := "/tmp/brolang-test-direct-root" app := "/tmp/brolang-test-direct-root/app" dep := "/tmp/brolang-test-direct-root/dep" output := "/tmp/brolang-test-direct-root-out" _ = os2.remove_all(root) _ = os.remove(output) defer _ = os2.remove_all(root) defer _ = os.remove(output) testing.expect(t, os2.make_directory_all(app) == nil) testing.expect(t, os2.make_directory_all(dep) == nil) main_text := "dep :: import \"@dep\"\nmain func() i32 { return dep.value }\n" dep_text := "value i32 :: 7\n" testing.expect(t, os.write_entire_file("/tmp/brolang-test-direct-root/app/main.bro", transmute([]byte)main_text)) testing.expect(t, os.write_entire_file("/tmp/brolang-test-direct-root/dep/dep.bro", transmute([]byte)dep_text)) testing.expect_value(t, compiler_core.compile_package(app, output), 1) status := compiler_core.compile_package(app, output, nil, target.DEFAULT, cimport.Options{}, root) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 7) } @(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) milestone_24_regressions_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-milestone-24" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/milestone_24", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) milestone_24_rejects_invalid_forms :: proc(t: ^testing.T) { Case :: struct { directory: string, output: string, } cases := [?]Case{ {"examples/programs/index_signed_error", "/tmp/brolang-test-index-signed-error"}, {"examples/programs/index_int_constraint_error", "/tmp/brolang-test-index-int-constraint-error"}, {"examples/programs/scalar_cast_error", "/tmp/brolang-test-scalar-cast-error"}, {"examples/programs/array_const_size_error", "/tmp/brolang-test-array-const-size-error"}, } for test_case in cases { status := compiler_core.compile_package(test_case.directory, test_case.output) testing.expect_value(t, status, 1) _ = os.remove(test_case.output) } } @(test) function_literals_lower_as_function_pointer_values :: proc(t: ^testing.T) { text := `Callbacks :: struct { call @func(value i32) i32 value i32 } run func(callbacks Callbacks) i32 { return callbacks.call(callbacks.value) } main func() i32 { callbacks Callbacks = Callbacks { call = func(value i32) i32 { return value + 1 }, value = 41, } return run(callbacks) - 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) ir_module := lower.lower(&hir_module) defer ir.destroy_module(&ir_module) generated_functions := 0 literal_exprs := 0 for function in ast_module.functions { generated_functions += 1 if function.generated else 0 } for expr in ast_module.exprs { literal_exprs += 1 if expr.kind == .Function_Literal else 0 } indirect_calls := 0 for expr in hir_module.exprs { if expr.kind == .Call && expr.left != hir.INVALID_EXPR { indirect_calls += 1 } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, generated_functions, 1) testing.expect_value(t, literal_exprs, 1) testing.expect(t, len(ir_module.functions) > 0) testing.expect(t, indirect_calls > 0) } @(test) qualified_value_calls_do_not_imply_package_resolution :: proc(t: ^testing.T) { text := `Callbacks :: struct { call @func() void } main func() void { callbacks Callbacks = Callbacks{call = func() void {}} callbacks.call() missing.call() } ` 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), 1) testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'") testing.expect(t, !strings.contains(diagnostics.items[0].message, "package")) } @(test) function_literals_do_not_capture_locals :: proc(t: ^testing.T) { text := `main func() i32 { offset i32 = 1 callback @func(value i32) i32 = func(value i32) i32 { return value + offset } return callback(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 := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "unknown symbol 'offset'") } testing.expect(t, found) } @(test) field_function_pointer_calls_reject_non_callable_fields :: proc(t: ^testing.T) { text := `Box :: struct { value i32 } main func() void { box Box = Box { value = 1 } box.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) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "call target is not a function pointer") } testing.expect(t, found) } @(test) allocator_contract_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-mem-allocator" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/mem_allocator", output, nil, target.DEFAULT, cimport.Options{}, ".") testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) allocator_contract_c_allocator_global_lowers :: 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/programs/mem_allocator", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".") defer ast.destroy_module(&ast_module) testing.expect(t, loaded) 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) found_c_allocator := false found_anyopaque_context := false found_vtable_pointer := false found_alloc_callback := false found_realloc_callback := false found_free_callback := false for global in hir_module.globals { if symbol.resolve(&symbols, global.name) != "c_allocator" { continue } found_c_allocator = true for field in types.fields_for(&hir_module.types, global.type) { name := symbol.resolve(&symbols, symbol.Id(field.name)) if name == "context" { optional_item, optional_ok := types.node(&hir_module.types, field.type) if optional_ok && optional_item.kind == .Optional { pointer_item, pointer_ok := types.node(&hir_module.types, optional_item.child) found_anyopaque_context = pointer_ok && pointer_item.kind == .Pointer && pointer_item.mutable && !pointer_item.many && pointer_item.child == types.ANYOPAQUE } } else if name == "vtable" { pointer_item, pointer_ok := types.node(&hir_module.types, field.type) found_vtable_pointer = pointer_ok && pointer_item.kind == .Pointer && !pointer_item.mutable && !pointer_item.many if found_vtable_pointer { for callback in types.fields_for(&hir_module.types, pointer_item.child) { callback_name := symbol.resolve(&symbols, symbol.Id(callback.name)) callback_pointer, _, _, callable := types.function_pointer(callback.type, &hir_module.types) found_alloc_callback = found_alloc_callback || callback_name == "alloc" && callable && !callback_pointer.many found_realloc_callback = found_realloc_callback || callback_name == "realloc" && callable && !callback_pointer.many found_free_callback = found_free_callback || callback_name == "free" && callable && !callback_pointer.many } } } } } testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, len(ir_module.functions) > 0) testing.expect(t, found_c_allocator) testing.expect(t, found_anyopaque_context) testing.expect(t, found_vtable_pointer) testing.expect(t, found_alloc_callback) testing.expect(t, found_realloc_callback) testing.expect(t, found_free_callback) } @(test) milestone_25_c_allocator_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-c-allocator" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/mem_allocator", output, nil, target.DEFAULT, cimport.Options{}, ".") testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) milestone_25_c_allocator_emits_libc_alloc_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) ast_module, loaded := loader.load("examples/programs/mem_allocator", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".") defer ast.destroy_module(&ast_module) testing.expect(t, loaded) 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 ptr @malloc(i64)")) testing.expect(t, strings.contains(llvm_text, "declare ptr @realloc(ptr, i64)")) testing.expect(t, strings.contains(llvm_text, "declare i32 @posix_memalign(ptr, i64, i64)")) testing.expect(t, strings.contains(llvm_text, "declare void @free(ptr)")) } @(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() bad_errdefer_nonfallible() _ = bad_try_in_defer() catch 0 _ = bad_try_in_errdefer() catch 0 return 0 } Failure :: enum { bad } fail func() i32 ! Failure { return .bad } 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 } } bad_errdefer_nonfallible func() void { errdefer {} } bad_try_in_defer func() i32 ! Failure { defer _ = try fail() return 1 } bad_try_in_errdefer func() i32 ! Failure { errdefer _ = try fail() 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) defer_return := false defer_break := false return_in_defer := false errdefer_nonfallible := false try_in_defer := 0 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'") errdefer_nonfallible = errdefer_nonfallible || strings.contains(diagnostic.message, "'errdefer' requires an enclosing fallible function") try_in_defer += 1 if strings.contains(diagnostic.message, "cannot 'try' inside a 'defer'") else 0 } testing.expect(t, defer_return) testing.expect(t, defer_break) testing.expect(t, return_in_defer) testing.expect(t, errdefer_nonfallible) testing.expect_value(t, try_in_defer, 2) } @(test) errdefer_capture_syntax_is_diagnosed :: proc(t: ^testing.T) { text := `Failure :: enum { bad } bad func() i32 ! Failure { errdefer || {} errdefer |first, second| {} return .bad } main func() i32 { return bad() catch 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) module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&module) missing := false multiple := false for diagnostic in diagnostics.items { missing = missing || strings.contains(diagnostic.message, "expected an errdefer capture name") multiple = multiple || strings.contains(diagnostic.message, "'errdefer' accepts exactly one capture") } testing.expect(t, missing) testing.expect(t, multiple) } @(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) value_loop_label_does_not_shadow_own_yield_target :: proc(t: ^testing.T) { text := `main func() i32 { idx :: for 0..10 |i| hit: { if (i == 3) yield :hit i yield none } if idx |found| { if (found == 3) return 0 return 1 } return 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) testing.expect_value(t, len(diagnostics.items), 0) } @(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) fallible_ergonomics_rejects_bad_try_and_catch_blocks :: proc(t: ^testing.T) { text := `A :: enum { a } B :: enum { b } fa func() i32 ! A { return .a } bad_error func() i32 ! B { x :: try fa() return x } bad_catch func() i32 { return fa() catch |e| { _ = e } } bad_nested_match func() i32 { return fa() catch |e| { match e { .a: yield 3 } } } main func() i32 { _ = bad_error() catch 0 return bad_catch() + bad_nested_match() } ` source_file := source.Source{path="fallible_ergonomics.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_error := false found_yield := false found_misplaced_yield := false for diagnostic in diagnostics.items { found_error = found_error || strings.contains(diagnostic.message, "'try' error channel cannot be widened") found_yield = found_yield || strings.contains(diagnostic.message, "a value block must end with an explicit 'yield'") found_misplaced_yield = found_misplaced_yield || strings.contains(diagnostic.message, "'yield' is only valid as the final statement") } testing.expect(t, found_error) testing.expect(t, found_yield) testing.expect(t, found_misplaced_yield) success_text := `A :: enum { a } B :: enum { b } Both :: alias A | B fs func() i64 ! A { return 1 } bad_success func() i32 ! Both { x :: try fs() return x } main func() i32 { return bad_success() catch 0 } ` directory := "/tmp/brolang-test-try-success-mismatch" main_path := "/tmp/brolang-test-try-success-mismatch/main.bro" output := "/tmp/brolang-test-try-success-mismatch-output" _ = 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)success_text)) testing.expect_value(t, compiler_core.compile_package(directory, output), 1) } @(test) terminating_catch_block_compiles_and_runs :: proc(t: ^testing.T) { text := `Failure :: enum { bad } may_fail func(fail bool) i32 ! Failure { if (fail) return .bad return 1 } recover func(fail bool) i32 { value :: may_fail(fail) catch |_| { return 40 } return value + 1 } main func() i32 { return recover(false) + recover(true) - 42 } ` directory := "/tmp/brolang-test-terminating-catch" main_path := "/tmp/brolang-test-terminating-catch/main.bro" output := "/tmp/brolang-test-terminating-catch-output" _ = 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)) testing.expect_value(t, compiler_core.compile_package(directory, output), 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) fallible_void_fallthrough_compiles_at_runtime_and_comptime :: proc(t: ^testing.T) { text := `Failure :: enum { bad } succeed func() void ! Failure {} fail func() void ! Failure { return .bad } maybe func(should_fail bool) void ! Failure { if should_fail { return .bad } } recover func(exit bool) i32 { fail() catch |_| { if exit { return 7 } } return 0 } comptime_scenario func() i32 { succeed() catch |_| {} fail() catch |_| {} return 0 } main func() i32 { comptime_result i32 :: $comptime_scenario() handled bool = false fail() catch |_| { handled = true } if !handled { return 3 } succeed() catch |_| { return 1 } maybe(false) catch |_| { return 2 } return comptime_result + recover(false) + recover(true) - 7 } ` directory := "/tmp/brolang-test-fallible-void-fallthrough" main_path := "/tmp/brolang-test-fallible-void-fallthrough/main.bro" output := "/tmp/brolang-test-fallible-void-fallthrough-output" _ = 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)) testing.expect_value(t, compiler_core.compile_package(directory, output), 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) conversion_diagnostics_render_source_types :: proc(t: ^testing.T) { text := `Allocator :: struct { marker i32 } take func(allocator Allocator, memory []mut u8) void {} main func() void { allocator Allocator = Allocator { marker = 0 } data [1]mut u8 = [0] take(data[..], allocator) } ` source_file := source.Source{path="type_labels.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) slice_to_allocator := false allocator_to_slice := false internal_type_id := false for diagnostic in diagnostics.items { slice_to_allocator = slice_to_allocator || strings.contains(diagnostic.message, "cannot implicitly convert []mut u8 to Allocator") allocator_to_slice = allocator_to_slice || strings.contains(diagnostic.message, "cannot implicitly convert Allocator to []mut u8") internal_type_id = internal_type_id || strings.contains(diagnostic.message, " 0) } @(test) inline_fallible_error_types_compile :: proc(t: ^testing.T) { text := `inline_enum func(value i32) i32 ! enum { inline_bad inline_worse } { if (value == 0) return .inline_bad if (value < 0) return .inline_worse return value } inline_union func(value i32) i32 ! union(enum) { inline_code i32 inline_empty void } { if (value == 0) return .inline_code{6} if (value == 1) return .inline_empty return value } main func() i32 { a :: inline_enum(0) catch |e| { result i32 :: if (e == .inline_bad) { yield 10 } else { yield 11 } yield result } b :: inline_union(0) catch |e| { result i32 :: match e { .inline_code |n|: n .inline_empty: 12 } yield result } return a + b - 16 } ` source_file := source.Source{path="expand_errors.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, len(hir_module.functions) > 1) testing.expect(t, len(llvm_text) > 0) } @(test) contextual_payload_variants_reject_bad_forms :: proc(t: ^testing.T) { text := `DetailError :: union(enum) { code i32 empty void } missing_payload func() DetailError { return .code } void_payload func() DetailError { return .empty{1} } empty_payload func() DetailError { return .code{} } multi_payload func() DetailError { return .code{1, 2} } unknown_payload func() DetailError { return .missing{1} } no_context func() i32 { _ = .code{1} return 0 } main func() i32 { _ = missing_payload() _ = void_payload() _ = empty_payload() _ = multi_payload() _ = unknown_payload() return no_context() } ` source_file := source.Source{path="bad_contextual_payload.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_missing := false found_void := false payload_arity_errors := 0 found_unknown := false found_no_context := false for diagnostic in diagnostics.items { found_missing = found_missing || strings.contains(diagnostic.message, "needs a payload") found_void = found_void || strings.contains(diagnostic.message, "void variant 'empty' takes no value") payload_arity_errors += 1 if strings.contains(diagnostic.message, "requires exactly one expression") else 0 found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown variant '.missing'") found_no_context = found_no_context || strings.contains(diagnostic.message, "requires a tagged-union context") } testing.expect(t, found_missing) testing.expect(t, found_void) testing.expect(t, payload_arity_errors >= 2) testing.expect(t, found_unknown) testing.expect(t, found_no_context) } @(test) anonymous_struct_payloads_reject_bad_forms :: proc(t: ^testing.T) { text := `Bad :: union(enum) { payload struct { x i32 y i32 } scalar i32 } unknown func() Bad { return .payload{x = 1, z = 2} } duplicate func() Bad { return .payload{x = 1, x = 2, y = 3} } missing func() Bad { return .payload{x = 1} } scalar_keyed func() Bad { return .scalar{value = 1} } A :: union(enum) { dup struct { x i32 } } B :: union(enum) { dup struct { x i64 } } Conflict :: alias A | B main func() i32 { _ = unknown() _ = duplicate() _ = missing() _ = scalar_keyed() return 0 } ` source_file := source.Source{path="bad_anonymous_payloads.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_unknown := false found_duplicate := false found_missing := false found_keyed_non_struct := false found_conflict := false for diagnostic in diagnostics.items { found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown struct field 'z'") found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate initializer for struct field 'x'") found_missing = found_missing || strings.contains(diagnostic.message, "missing initializer for struct field 'y'") found_keyed_non_struct = found_keyed_non_struct || strings.contains(diagnostic.message, "keyed contextual payload requires a struct payload") found_conflict = found_conflict || strings.contains(diagnostic.message, "same variant name") } testing.expect(t, found_unknown) testing.expect(t, found_duplicate) testing.expect(t, found_missing) testing.expect(t, found_keyed_non_struct) testing.expect(t, found_conflict) } @(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) 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) 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_prefix := []string{ "/usr/bin/env", "zig", "cc", "-target", "aarch64-macos", "-Wno-override-module", "-Wno-unused-command-line-argument", } testing.expect(t, len(command) >= len(expected_prefix)) for value, index in expected_prefix { testing.expect_value(t, command[index], value) } tail_index := len(expected_prefix) if sdk, ok := backend.macos_sdk_root(); ok { defer delete(sdk) frameworks := fmt.tprintf("-F%s/System/Library/Frameworks", sdk) lib_dir := fmt.tprintf("-L%s/usr/lib", sdk) testing.expect(t, len(command) >= tail_index + 2) testing.expect_value(t, command[tail_index], frameworks) testing.expect_value(t, command[tail_index + 1], lib_dir) tail_index += 2 } expected_tail := []string{ "module.ll", "-Ivendor/include", "-DFEATURE=1", "native.c", "-Lvendor/lib", "-lthing", "helper.o", "-o", "program", } testing.expect_value(t, len(command), tail_index + len(expected_tail)) for value, index in expected_tail { testing.expect_value(t, command[tail_index + 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) diagnostic_warnings_format_and_dedupe_by_severity :: proc(t: ^testing.T) { source_file := source.Source{path="test.bro", text="one\n"} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) span := source.Span{start=0, end=3} warning := source.add_warning(&diagnostics, span, "same") warning_again := source.addf_warning(&diagnostics, span, "%s", "same") err := source.add(&diagnostics, span, "same") formatted_warning := source.format(&diagnostics, warning) defer delete(formatted_warning) formatted_error := source.format(&diagnostics, err) defer delete(formatted_error) testing.expect_value(t, warning, warning_again) testing.expect(t, warning != err) testing.expect_value(t, len(diagnostics.items), 2) testing.expect_value(t, diagnostics.items[warning].severity, source.Severity.Warning) testing.expect_value(t, diagnostics.items[err].severity, source.Severity.Error) testing.expect(t, strings.contains(formatted_warning, "warning: same\n --> test.bro:1:1")) testing.expect(t, strings.contains(formatted_error, "error: same\n --> test.bro:1:1")) } @(test) rich_diagnostics_render_labels_notes_help_and_tabs :: proc(t: ^testing.T) { store := source.init_store() defer source.destroy_store(&store) primary_file := source.add_source(&store, "main.bro", "\tmissing()\nnext()\n") definition_file := source.add_source(&store, "dep.bro", "value :: 1\n") diagnostics := source.init_store_diagnostics(&store) defer source.destroy_diagnostics(&diagnostics) id := source.add(&diagnostics, source.Span{file=primary_file, start=1, end=8}, "unknown symbol 'missing'") source.set_primary_label(&diagnostics, id, "unknown symbol") source.set_primary_label(&diagnostics, id, "unknown symbol") source.add_secondary_label( &diagnostics, id, source.Span{file=definition_file, start=0, end=5}, "related declaration", ) source.add_note(&diagnostics, id, "names resolve in the current scope") source.add_note(&diagnostics, id, "names resolve in the current scope") source.add_help(&diagnostics, id, "declare 'missing' before using it") formatted := source.format(&diagnostics, id) defer delete(formatted) multiline := source.add( &diagnostics, source.Span{file=primary_file, start=1, end=17}, "multiline failure", ) multiline_formatted := source.format(&diagnostics, multiline) defer delete(multiline_formatted) unknown := source.add(&diagnostics, source.Span{}, "no location") unknown_formatted := source.format(&diagnostics, unknown) defer delete(unknown_formatted) testing.expect_value(t, len(diagnostics.annotations), 4) testing.expect(t, strings.contains(formatted, "error: unknown symbol 'missing'")) testing.expect(t, strings.contains(formatted, "--> main.bro:1:2")) testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol")) testing.expect(t, strings.contains(formatted, "::: dep.bro:1:1")) testing.expect(t, strings.contains(formatted, "----- related declaration")) testing.expect(t, strings.contains(formatted, "note: names resolve in the current scope")) testing.expect(t, strings.contains(formatted, "help: declare 'missing' before using it")) testing.expect(t, strings.contains(multiline_formatted, "1 | missing()")) testing.expect(t, !strings.contains(multiline_formatted, "next()")) testing.expect_value(t, unknown_formatted, "main.bro: error: no location") } @(test) poisoned_expressions_preserve_independent_root_diagnostics :: proc(t: ^testing.T) { text := `bad :: missing_global sink func(value i32) void { _ = value } broken func(value int) int { return missing_return + value } main func() void { _ = missing_add + 1 _ = try missing_try() missing_catch() catch |_| {} sink(missing_arg) missing_stmt() missing_target.field = 1 value i32 = 0 value += missing_rhs _ = broken(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), 9) for diagnostic in diagnostics.items { testing.expect(t, strings.has_prefix(diagnostic.message, "unknown symbol 'missing_")) testing.expect(t, !strings.contains(diagnostic.message, "fallible expression")) testing.expect(t, !strings.contains(diagnostic.message, "must be consumed")) testing.expect(t, !strings.contains(diagnostic.message, "compatible numeric operands")) testing.expect(t, !strings.contains(diagnostic.message, "implicitly convert")) } } @(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 :: divtrunc!(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 builtin denominator is zero") 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_expression_range := 0 found_literal_range := 0 found_u64_range := false for diagnostic in diagnostics.items { found_expression_range += 1 if strings.contains(diagnostic.message, "exceeds signed i64 range") else 0 found_literal_range += 1 if strings.contains(diagnostic.message, "does not fit in i64") else 0 found_u64_range = found_u64_range || strings.contains(diagnostic.message, "magnitude does not fit in u64") } testing.expect_value(t, found_expression_range, 2) testing.expect_value(t, found_literal_range, 2) 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, 11) 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} division_ops := [?]ir.Opcode{ .Div_Checked, .Div_Trunc_Checked, .Div_Floor_Checked, .Div_Exact_Checked, .Div_Ceil_Checked, .Rem_Checked, .Mod_Checked, } for op, index in division_ops { instructions[3+index] = ir.Instruction{op=op, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC} } instructions[10] = 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")) for index in 3..=9 { testing.expect(t, strings.contains(text, fmt.tprintf("%%v%d = add i32 0, -1431655766", index))) } 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) global_cycle_diagnostic_reports_endpoint_lines :: proc(t: ^testing.T) { text := `a i32 :: b b i32 :: 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) found := false for _, diagnostic_index in diagnostics.items { message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index)) found = found || strings.contains(message, "global initialization cycle from 'a' at test.bro:1 to 'b' at test.bro:2") delete(message) } testing.expect(t, found) } @(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) } } @(test) hide_declarations_are_file_hidden :: proc(t: ^testing.T) { output := "/tmp/brolang-test-hidden-declarations" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/hidden_valid/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 9) } @(test) file_hidden_declarations_are_not_package_members :: 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/hidden_invalid/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_sibling := false found_sibling_value := false found_sibling_type := false found_import := false found_collision := false for diagnostic in diagnostics.items { found_sibling = found_sibling || strings.contains(diagnostic.message, "unknown symbol 'sibling'") found_sibling_value = found_sibling_value || strings.contains(diagnostic.message, "unknown symbol 'sibling_value'") found_sibling_type = found_sibling_type || strings.contains(diagnostic.message, "unknown or opaque record type 'Sibling'") found_import = found_import || strings.contains(diagnostic.message, "package 'dep' has no member 'secret'") found_collision = found_collision || strings.contains(diagnostic.message, "duplicate function 'collision'") } testing.expect(t, found_sibling) testing.expect(t, found_sibling_value) testing.expect(t, found_sibling_type) testing.expect(t, found_import) testing.expect(t, found_collision) } @(test) package_import_cycles_are_valid :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-cycle" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/cycle/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 4) } @(test) imported_main_is_an_ordinary_callable_function :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-imported-main" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/imported_main/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 7) } @(test) same_named_c_functions_in_packages_do_not_collide :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-c-symbols" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/c_symbols/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) invalid_root_package_preserves_existing_output :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-preserved-output" defer _ = os.remove(output) previous := "previous artifact" testing.expect(t, os.write_entire_file(output, transmute([]byte)previous)) testing.expect_value(t, compiler_core.compile_package("examples/programs/prototype/main.bro", output), 2) data, ok := os.read_entire_file(output) defer delete(data) testing.expect(t, ok) testing.expect_value(t, string(data), previous) } @(test) empty_root_package_is_an_infrastructure_error :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-empty" defer _ = os.remove(output) testing.expect_value(t, compiler_core.compile_package("examples/packages/empty", output), 2) testing.expect(t, !os.exists(output)) } @(test) same_package_can_be_imported_under_distinct_aliases :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-aliases" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/aliases/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 4) } @(test) duplicate_same_file_import_keeps_first_binding :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-duplicate" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/duplicate/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 2) } @(test) identical_imports_in_different_files_are_independent :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-repeated" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/repeated/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 4) } @(test) imports_do_not_reexport_members :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-non-transitive" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/non_transitive/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) declaration_aliases_preserve_identity_and_chain :: proc(t: ^testing.T) { root :: "/tmp/brolang-test-declaration-aliases" dep_dir :: root + "/dep" facade_dir :: root + "/facade" top_dir :: root + "/top" app_dir :: root + "/app" output :: "/tmp/brolang-test-declaration-aliases-output" _ = os2.remove_all(root) defer _ = os2.remove_all(root) defer _ = os.remove(output) directories := [?]string{root, dep_dir, facade_dir, top_dir, app_dir} for directory in directories { testing.expect(t, os.make_directory(directory) == nil) } dep_text := `Box func($T type) type { return struct { value T } } Point :: struct { value i32 } counter i32 = 1 answer func() i32 { return 40 } ` facade_text := `dep :: import "../dep" RenamedBox :: alias dep.Box RenamedPoint :: alias dep.Point counter :: alias dep.counter answer :: alias dep.answer hide local_answer_alias :: alias dep.answer local_answer func() i32 { return local_answer_alias() } Scalar :: alias i32 MaybePoint :: alias ?@dep.Point Concrete :: alias dep.Box(i32) ` top_text := `facade :: import "../facade" Box :: alias facade.RenamedBox Point :: alias facade.RenamedPoint counter :: alias facade.counter answer :: alias facade.answer ` app_text := `dep :: import "../dep" facade :: import "../facade" top :: import "../top" main func() i32 { box top.Box(i32) :: top.Box(i32) { value = 2 } point top.Point :: top.Point { value = 3 } maybe facade.MaybePoint :: none scalar facade.Scalar :: 5 top.counter = 7 if box.value != 2 or point.value != 3 { return 1 } if scalar != 5 or top.answer() != 40 or facade.local_answer() != 40 or dep.counter != 7 { return 2 } _ = maybe return 0 } ` testing.expect(t, os.write_entire_file(dep_dir + "/dep.bro", transmute([]byte)dep_text)) testing.expect(t, os.write_entire_file(facade_dir + "/facade.bro", transmute([]byte)facade_text)) testing.expect(t, os.write_entire_file(top_dir + "/top.bro", transmute([]byte)top_text)) testing.expect(t, os.write_entire_file(app_dir + "/main.bro", transmute([]byte)app_text)) status := compiler_core.compile_package(app_dir, output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) declaration_aliases_diagnose_invalid_targets :: proc(t: ^testing.T) { root :: "/tmp/brolang-test-declaration-alias-errors" dep_dir :: root + "/dep" facade_dir :: root + "/facade" a_dir :: root + "/a" b_dir :: root + "/b" app_dir :: root + "/app" _ = os2.remove_all(root) defer _ = os2.remove_all(root) directories := [?]string{root, dep_dir, facade_dir, a_dir, b_dir, app_dir} for directory in directories { testing.expect(t, os.make_directory(directory) == nil) } dep_text := `visible func() i32 { return 1 } hide hidden_target func() i32 { return 2 } ambiguous func() i32 { return 3 } ambiguous i32 :: 4 ` facade_text := `dep :: import "../dep" gone :: import "../gone" missing :: alias dep.missing hidden :: alias dep.hidden_target unknown :: alias nope.visible unavailable :: alias gone.visible ambiguous :: alias dep.ambiguous duplicate :: alias dep.visible duplicate :: alias dep.visible collision func() i32 { return 0 } collision :: alias dep.visible dep :: alias dep.visible ` a_text := `b :: import "../b" value :: alias b.value ` b_text := `a :: import "../a" value :: alias a.value ` app_text := `facade :: import "../facade" a :: import "../a" main func() void {} ` testing.expect(t, os.write_entire_file(dep_dir + "/dep.bro", transmute([]byte)dep_text)) testing.expect(t, os.write_entire_file(facade_dir + "/facade.bro", transmute([]byte)facade_text)) testing.expect(t, os.write_entire_file(a_dir + "/a.bro", transmute([]byte)a_text)) testing.expect(t, os.write_entire_file(b_dir + "/b.bro", transmute([]byte)b_text)) testing.expect(t, os.write_entire_file(app_dir + "/main.bro", transmute([]byte)app_text)) 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(app_dir, &sources, &diagnostics, &symbols) defer ast.destroy_module(&module) testing.expect(t, loaded) wants := []string{ "has no member 'missing'", "is file-hidden", "unknown symbol 'nope'", "unavailable imported package 'gone'", "package member 'dep.ambiguous' is ambiguous", "duplicate declaration alias 'duplicate'", "declaration alias 'collision' conflicts with a package declaration", "declaration alias 'dep' conflicts with an import", "declaration alias cycle", } for want in wants { found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, want) } testing.expect(t, found) } } @(test) self_import_via_dot_is_valid :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-self" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/self_import/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 5) } @(test) unused_absolute_import_is_diagnosed_without_trapping :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-absolute" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/absolute/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) unreferenced_problematic_imported_global_is_deferred :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-problematic-unused" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/problematic_unused/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) valid_eager_global_in_unused_package_still_initializes :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-eager-unused" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/eager_unused/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) cross_package_global_initialization_cycle_traps_when_used :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-global-cycle" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/global_cycle/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) cross_package_generic_specializes_from_folded_argument :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-generic" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/generic/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 128) } @(test) comptime_value_params_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-comptime-value-params" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/comptime_value_params", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) comptime_type_params_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-comptime-type-params" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/comptime_type_params", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) type_factories_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-type-factory" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/type_factory", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) arraylist_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-arraylist" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/arraylist", output, nil, target.DEFAULT, cimport.Options{}, ".") testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) type_factory_rejects_runtime_parameters_and_recursion :: proc(t: ^testing.T) { texts := []string{ `Bad func($T type, n usize) type { return struct { value [n]T } } main func() void { value Bad(i32, 4) = undefined; _ = &value } `, `Loop func($T type) type { return struct { next @Loop(T) } } main func() void { value Loop(i32) = undefined; _ = &value } `, `Box func($T type) type { return struct { value T } } main func() void { _ = Box(i32) } `, `Bad func($T type) type { return 1 } main func() void { value Bad(i32) = undefined; _ = &value } `, } wanted := []string{"must be comptime", "recursive type-factory specialization", "only valid in type position", "cannot implicitly convert"} for text, index in texts { source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) symbols := symbol.init_table() stream := lexer.lex(&source_file, &diagnostics, &symbols) ast_module := parser.parse(&stream, &source_file, &diagnostics) hir_module := checker.check(&ast_module, &diagnostics, &symbols) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, wanted[index]) } testing.expect(t, found) hir.destroy_module(&hir_module) ast.destroy_module(&ast_module) delete(stream.items) symbol.destroy_table(&symbols) source.destroy_diagnostics(&diagnostics) } } @(test) comptime_eval_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-comptime-eval" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/comptime_eval", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) comptime_v1_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-comptime-v1" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/comptime_v1", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) lazy_function_body_marks_import_as_used_without_resolving_it :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-lazy-import" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/lazy_import/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) qualified_global_inference_ignores_same_named_local :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-qualified-shadow" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/qualified_shadow/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 300) } @(test) cross_package_recursive_specialization_reaches_fixed_point :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-recursive" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/recursive/app", output) testing.expect_value(t, status, 0) testing.expect(t, os.exists(output)) } @(test) imported_main_does_not_satisfy_root_main_requirement :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-missing-root-main" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/missing_root_main/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) package_llvm_is_deterministic_and_symbols_include_package_ids :: 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/c_symbols/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) second_llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) defer delete(second_llvm_text) testing.expect(t, loaded) 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__p1__same")) testing.expect(t, strings.contains(llvm_text, "define signext i8 @bro_c__p2__same")) testing.expect(t, strings.contains(llvm_text, "define i32 @main()")) } @(test) invalid_default_alias_requires_an_explicit_alias :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-default-alias-invalid" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/default_alias_invalid/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) explicit_alias_allows_invalid_directory_basename :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-explicit-alias" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/explicit_alias/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 3) } @(test) import_alias_conflict_is_diagnosed_without_trapping :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-alias-conflict" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/alias_conflict/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 7) } @(test) empty_imported_package_is_a_source_diagnostic :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-empty-import" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/empty_import/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) non_directory_import_is_a_source_diagnostic :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-file-import" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/file_import/app", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) import_is_available_before_its_textual_declaration :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-import-order" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/import_order/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 6) } @(test) deeply_nested_child_packages_load_recursively :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-deep" defer _ = os.remove(output) status := compiler_core.compile_package("examples/packages/deep/app", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 9) } @(test) function_returning_only_in_if_branch_is_diagnosed :: proc(t: ^testing.T) { text := `classify func(n i32) i32 { if n > 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) parser_diagnoses_braceless_for_and_unwrap_without_parens_or_call :: proc(t: ^testing.T) { text := `main func() void { for tokens.items |item| _ = item value ?i32 = 1 if value |present| _ = present } ` 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), 2) testing.expect(t, strings.contains(diagnostics.items[0].message, "brace-less 'for'")) testing.expect(t, strings.contains(diagnostics.items[1].message, "brace-less 'if'")) } @(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, "unknown symbol '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, "unknown symbol '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, "unknown symbol '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) parser_accepts_braceless_for_bodies :: proc(t: ^testing.T) { text := `make_items func() [2]i32 { return [1, 2] } main func() void { for (tokens.items) |item| _ = item for make_items() |item| _ = item } ` 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), 2) parenthesized := module.statements[main.body[0]] call := module.statements[main.body[1]] testing.expect_value(t, len(parenthesized.body), 1) testing.expect(t, module.exprs[parenthesized.expr].parenthesized) testing.expect_value(t, len(call.body), 1) testing.expect_value(t, module.exprs[call.expr].kind, ast.Expr_Kind.Call) } @(test) braceless_for_compiles_and_runs :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-braceless-for" main_path := "/tmp/brolang-test-braceless-for/main.bro" output := "/tmp/brolang-test-braceless-for-output" text := `make_items func() [2]i32 { return [20, 2] } main func() i32 { total i32 = 0 items :: [10, 11] for (items) |item| total += item for make_items() |item| total += item return total } ` _ = 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, 43) } @(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, "unknown symbol '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 _ = point _ = pointer _ = maybe } ` 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 = divtrunc!(signed, 4) unsigned u32 = 24 unsigned += 6 unsigned -= 2 unsigned *= 3 unsigned = divtrunc!(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], 1) add_count := 0 sub_count := 0 mul_count := 0 div_count := 0 div_trunc_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 .Div_Trunc_Checked: div_trunc_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, 1) testing.expect_value(t, div_trunc_count, 2) } @(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 divtrunc!(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) integer_slash_is_rejected_and_float_slash_remains_available :: proc(t: ^testing.T) { Case :: struct {text, want: string} invalid := []Case{ {text=`main func() void { a i32 = 4 b i32 = 2 _ = a / b }`, want="integer '/' is not allowed"}, {text=`main func() void { a u32 = 4 b u32 = 2 _ = a / b }`, want="integer '/' is not allowed"}, {text=`main func() void { _ = 4 / 2 }`, want="integer '/' is not allowed"}, {text=`main func() void { values [4 / 2]u8 = undefined _ = &values }`, want="integer '/' is not allowed"}, {text=`half func($value i32) i32 { return value / 2 } main func() void { _ = $half(4) }`, want="integer '/' is not allowed"}, {text=`main func() void { value i32 = 8 value /= 2 }`, want="assign through an explicit division builtin"}, } for test_case in invalid { 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) ast_module := parser.parse(&stream, &source_file, &diagnostics) hir_module := checker.check(&ast_module, &diagnostics, &symbols) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, test_case.want) } testing.expect(t, found) hir.destroy_module(&hir_module) ast.destroy_module(&ast_module) delete(stream.items) symbol.destroy_table(&symbols) source.destroy_diagnostics(&diagnostics) } text := `main func() void { value f32 = 5.0 / 2.0 value /= 2.0 _ = 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) testing.expect_value(t, len(diagnostics.items), 0) } @(test) division_builtins_diagnose_arity_operands_and_comptime_failures :: proc(t: ^testing.T) { text := `bad_arity :: divfloor!(1) bad_bool :: rem!(true, false) bad_family :: mod!(i32(5), f32(3)) zero_trunc :: divtrunc!(1, 0) zero_floor :: divfloor!(1.0, 0.0) zero_exact :: divexact!(1, 0) zero_ceil :: divceil!(1.0, 0.0) zero_rem :: rem!(1, 0) zero_mod :: mod!(1.0, 0.0) inexact :: divexact!(5, 3) overflow_trunc :: divtrunc!(minval!(i32), -1) overflow_floor :: divfloor!(minval!(i32), -1) overflow_exact :: divexact!(minval!(i32), -1) overflow_ceil :: divceil!(minval!(i32), -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) 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) zero_count := 0 found_arity, found_operands, found_exact := false, false, false overflow_count := 0 for diagnostic in diagnostics.items { found_arity = found_arity || strings.contains(diagnostic.message, "expects 2 arguments") found_operands = found_operands || strings.contains(diagnostic.message, "compatible numeric operands") found_exact = found_exact || strings.contains(diagnostic.message, "exact division has a remainder") if strings.contains(diagnostic.message, "signed integer division overflow") { overflow_count += 1 } if strings.contains(diagnostic.message, "division builtin denominator is zero") { zero_count += 1 } } testing.expect(t, found_arity) testing.expect(t, found_operands) testing.expect(t, found_exact) testing.expect_value(t, overflow_count, 4) testing.expect_value(t, zero_count, 6) } @(test) division_family_compiles_and_runs_for_integer_and_float_scalars :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-division-family" main_path := "/tmp/brolang-test-division-family/main.bro" output := "/tmp/brolang-test-division-family-output" text := `COUNT :: divexact!(8, 2) items [divceil!(10, 3)]u8 :: [0, 0, 0, 0] OPEN :: 5 open_ceil i32 :: divceil!(OPEN, 3) check_i32 func(a, b, qt, qf, qc, r, m i32) bool { return divtrunc!(a, b) == qt and divfloor!(a, b) == qf and divceil!(a, b) == qc and rem!(a, b) == r and mod!(a, b) == m } check_f32 func(a, b, qt, qf, qc, r, m f32) bool { return divtrunc!(a, b) == qt and divfloor!(a, b) == qf and divceil!(a, b) == qc and rem!(a, b) == r and mod!(a, b) == m } check_f64 func(a, b, qt, qf, qc, r, m f64) bool { return divtrunc!(a, b) == qt and divfloor!(a, b) == qf and divceil!(a, b) == qc and rem!(a, b) == r and mod!(a, b) == m } edge_rem func(a, b i32) i32 { return rem!(a, b) } edge_mod func(a, b i32) i32 { return mod!(a, b) } main func() i32 { if COUNT != 4 or items.len != 4 or open_ceil != 2 { return 1 } if !check_i32(5, 3, 1, 1, 2, 2, 2) { return 2 } if !check_i32(5, -3, -1, -2, -1, 2, -1) { return 3 } if !check_i32(-5, 3, -1, -2, -1, -2, 1) { return 4 } if !check_i32(-5, -3, 1, 1, 2, -2, -2) { return 5 } if divtrunc!(u32(5), u32(3)) != 1 or divfloor!(u32(5), u32(3)) != 1 or divceil!(u32(5), u32(3)) != 2 or rem!(u32(5), u32(3)) != 2 or mod!(u32(5), u32(3)) != 2 { return 6 } if divexact!(i32(6), i32(3)) != 2 or divexact!(u32(6), u32(3)) != 2 { return 7 } if !check_f32(f32(5.0), f32(3.0), f32(1.0), f32(1.0), f32(2.0), f32(2.0), f32(2.0)) or !check_f32(f32(5.0), f32(-3.0), f32(-1.0), f32(-2.0), f32(-1.0), f32(2.0), f32(-1.0)) or !check_f32(f32(-5.0), f32(3.0), f32(-1.0), f32(-2.0), f32(-1.0), f32(-2.0), f32(1.0)) or !check_f32(f32(-5.0), f32(-3.0), f32(1.0), f32(1.0), f32(2.0), f32(-2.0), f32(-2.0)) { return 8 } if !check_f64(5.0, 3.0, 1.0, 1.0, 2.0, 2.0, 2.0) or !check_f64(5.0, -3.0, -1.0, -2.0, -1.0, 2.0, -1.0) or !check_f64(-5.0, 3.0, -1.0, -2.0, -1.0, -2.0, 1.0) or !check_f64(-5.0, -3.0, 1.0, 1.0, 2.0, -2.0, -2.0) { return 9 } if divexact!(f32(6.0), f32(3.0)) != 2.0 or divexact!(f64(6.0), f64(3.0)) != 2.0 { return 10 } if edge_rem(-2147483648, -1) != 0 or edge_mod(-2147483648, -1) != 0 { return 11 } return 0 } ` _ = 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, 0) } @(test) division_builtins_trap_for_runtime_zero_overflow_and_inexact_results :: proc(t: ^testing.T) { Case :: struct { name: string, type_name: string, left: string, right: string, } cases := [?]Case{ {name="divtrunc", type_name="i32", left="1", right="0"}, {name="divfloor", type_name="f32", left="f32(1.0)", right="f32(0.0)"}, {name="divexact", type_name="f64", left="1.0", right="0.0"}, {name="divceil", type_name="i32", left="1", right="0"}, {name="rem", type_name="f32", left="f32(1.0)", right="f32(0.0)"}, {name="mod", type_name="f64", left="1.0", right="0.0"}, {name="divexact", type_name="i32", left="5", right="3"}, {name="divtrunc", type_name="i32", left="-2147483648", right="-1"}, {name="divfloor", type_name="i32", left="-2147483648", right="-1"}, {name="divexact", type_name="i32", left="-2147483648", right="-1"}, {name="divceil", type_name="i32", left="-2147483648", right="-1"}, } for test_case, index in cases { directory := fmt.aprintf("/tmp/brolang-test-division-trap-%d", index) main_path := fmt.aprintf("%s/main.bro", directory) output := fmt.aprintf("/tmp/brolang-test-division-trap-output-%d", index) text := fmt.aprintf( "invoke func(a, b %s) %s {{ return %s!(a, b) }}\nmain func() void {{ _ = invoke(%s, %s) }}\n", test_case.type_name, test_case.type_name, test_case.name, test_case.left, test_case.right, ) _ = 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) qualified_division_builtin_names_resolve_as_package_functions :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-qualified-division" math_directory := "/tmp/brolang-test-qualified-division/math" app_directory := "/tmp/brolang-test-qualified-division/app" math_path := "/tmp/brolang-test-qualified-division/math/math.bro" main_path := "/tmp/brolang-test-qualified-division/app/main.bro" output := "/tmp/brolang-test-qualified-division-output" math_text := `divfloor func(a, b i32) i32 { return a + b } ` main_text := `math :: import "../math" main func() i32 { return math.divfloor(20, 22) } ` _ = 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.make_directory(math_directory) == nil) testing.expect(t, os.make_directory(app_directory) == nil) testing.expect(t, os.write_entire_file(math_path, transmute([]byte)math_text)) testing.expect(t, os.write_entire_file(main_path, transmute([]byte)main_text)) status := compiler_core.compile_package(app_directory, output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 42) } @(test) division_builtins_emit_guards_rounding_and_single_integer_divisions :: proc(t: ^testing.T) { text := `floor_i32 func(a, b i32) i32 { return divfloor!(a, b) } ceil_i32 func(a, b i32) i32 { return divceil!(a, b) } exact_i32 func(a, b i32) i32 { return divexact!(a, b) } floor_u32 func(a, b u32) u32 { return divfloor!(a, b) } rem_i16 func(a, b i16) i16 { return rem!(a, b) } mod_i16 func(a, b i16) i16 { return mod!(a, b) } floor_f32 func(a, b f32) f32 { return divfloor!(a, b) } ceil_f64 func(a, b f64) f64 { return divceil!(a, b) } exact_f32 func(a, b f32) f32 { return divexact!(a, b) } rem_f64 func(a, b f64) f64 { return rem!(a, b) } mod_f32 func(a, b f32) f32 { return mod!(a, b) } main func() void { _ = floor_i32(5, 3) _ = ceil_i32(5, 3) _ = exact_i32(6, 3) _ = floor_u32(5, 3) _ = rem_i16(5, 3) _ = mod_i16(5, 3) _ = floor_f32(f32(5.0), f32(3.0)) _ = ceil_f64(5.0, 3.0) _ = exact_f32(f32(6.0), f32(3.0)) _ = rem_f64(5.0, 3.0) _ = mod_f32(f32(5.0), f32(3.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) testing.expect_value(t, strings.count(llvm_text, "sdiv i32"), 3) testing.expect(t, !strings.contains(llvm_text, "srem i32")) testing.expect(t, strings.contains(llvm_text, "udiv i32")) testing.expect(t, strings.contains(llvm_text, "srem i16")) testing.expect(t, strings.contains(llvm_text, "remspecial")) testing.expect(t, strings.contains(llvm_text, "divzero_trap")) testing.expect(t, strings.contains(llvm_text, "divovf_trap")) testing.expect(t, strings.contains(llvm_text, "call float @llvm.floor.f32")) testing.expect(t, strings.contains(llvm_text, "call double @llvm.ceil.f64")) testing.expect(t, strings.contains(llvm_text, "call float @llvm.trunc.f32")) testing.expect(t, strings.contains(llvm_text, "frem double")) } @(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 :: opaque 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) type_and_function_names_cannot_shadow :: 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 := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "function 'Value' shadows visible type") } testing.expect(t, found) } @(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) ordinal c_int :: c_int(number) _ = variadic(0, number) if take(value) == Animal.cat and values[0] != values[1] and number == Nat.two and ordinal == 2 { 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(t, strings.contains(llvm_text, "zext i16")) 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) keywords_are_valid_enum_members_and_tagged_union_variants :: proc(t: ^testing.T) { testing.expect(t, token.is_keyword(.Keyword_Func)) testing.expect(t, token.is_keyword(.Keyword_Hide)) testing.expect(t, token.is_keyword(.Keyword_C_Longdouble)) testing.expect(t, !token.is_keyword(.Identifier)) testing.expect(t, !token.is_keyword(.Underscore)) text := `TokenKind :: enum { if else return } Token :: union(TokenKind) { if i32 else void return i32 } kind func(value bool) TokenKind { if value { return .if } return TokenKind.else } main func() i32 { first TokenKind = kind(true) second TokenKind = .return a Token = Token{ if = 1 } b Token = Token{ else } c Token = .return{2} total i32 = a.if + c.return match first { .if: total = total + 1 .else: total = total + 2 .return: total = total + 3 } match b { .if |value|: total = total + value .else: total = total + 4 .return |value|: total = total + value } _ = second 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) testing.expect(t, len(llvm_text) > 0) } @(test) keyword_names_remain_invalid_for_struct_fields :: proc(t: ^testing.T) { text := `Bad :: struct { if 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 := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "expected a struct field name") } testing.expect(t, found) } @(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), // [5]=void, [6]=Pointer->void 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)}) append(&result.types, cimport.Type{kind = .Void, child = cimport.INVALID_TYPE}) append(&result.types, cimport.Type{kind = .Pointer, child = cimport.Type_Id(5), mutable = true}) // 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}) // record 2: incomplete struct -> opaque append(&result.records, cimport.Record{name = "Handle", kind = .Struct, complete = false}) // typedef aliases: a scalar, a callback function pointer, and a C void 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)}) append(&result.aliases, cimport.Alias{name = "RawPtr", type = cimport.Type_Id(6)}) 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)}) raw_params := []cimport.Type_Id{cimport.Type_Id(6)} raw_param_names := []string{"ptr"} append(&result.functions, cimport.Function{name = "consume_raw", params = raw_params, param_names = raw_param_names, result = cimport.Type_Id(5)}) 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")) testing.expect(t, strings.contains(output, "RawPtr :: alias ?*mut anyopaque")) testing.expect(t, strings.contains(output, "Handle :: opaque")) // 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, "consume_raw c_func(ptr ?*mut anyopaque) void")) 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) translate_c_package_deduplicates_compatible_declarations :: proc(t: ^testing.T) { first := cimport.init_result(context.allocator) second := cimport.init_result(context.allocator) defer { delete(first.types) delete(first.records[0].fields) delete(first.records) delete(first.functions) delete(first.macros) delete(second.types) delete(second.records[0].fields) delete(second.records) delete(second.functions) delete(second.macros) } append(&first.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE}) append(&first.types, cimport.Type{kind=.Record, record=0, child=cimport.INVALID_TYPE}) append(&first.types, cimport.Type{kind=.Pointer, child=1, mutable=true}) append(&second.types, ..first.types[:]) first_fields: [dynamic]cimport.Field second_fields: [dynamic]cimport.Field append(&first_fields, cimport.Field{name="value", type=0}) append(&second_fields, cimport.Field{name="value", type=0}) append(&first.records, cimport.Record{ identity="shared-anonymous-record", kind=.Struct, complete=true, fields=first_fields, }) append(&second.records, cimport.Record{ identity="shared-anonymous-record", kind=.Struct, complete=true, fields=second_fields, }) first_params := [?]cimport.Type_Id{0} second_params := [?]cimport.Type_Id{0} use_record_params := [?]cimport.Type_Id{2} first_names := [?]string{"left"} second_names := [?]string{"right"} append(&first.functions, cimport.Function{ name="shared", params=first_params[:], param_names=first_names[:], result=0, }) append(&second.functions, cimport.Function{ name="shared", params=second_params[:], param_names=second_names[:], result=0, }) append(&second.functions, cimport.Function{ name="use_record", params=use_record_params[:], result=0, }) append(&first.macros, cimport.Macro_Constant{ name="SHARED_VALUE", type=0, value={kind=.Integer, type=0, integer=7}, }) append(&second.macros, first.macros[0]) inputs := [?]translatec.Package_Input{ {result=&first, header="first.h", name="first.bro"}, {result=&second, header="second.h", name="second.bro"}, } outputs, generation_error := translatec.emit_package(inputs[:]) defer translatec.destroy_package_outputs(outputs) defer delete(generation_error) testing.expect_value(t, generation_error, "") testing.expect_value(t, len(outputs), 2) if len(outputs) == 2 { combined := fmt.tprintf("%s%s", outputs[0].source, outputs[1].source) testing.expect_value(t, count_substring_occurrences(combined, "shared c_func("), 1) testing.expect_value(t, count_substring_occurrences(combined, "SHARED_VALUE c_int :: 7"), 1) testing.expect(t, strings.contains(outputs[0].source, "__c_first_bro_record_0 :: c_struct")) testing.expect(t, strings.contains(outputs[1].source, "use_record c_func(_ ?*mut __c_first_bro_record_0) c_int")) } second.macros[0].value.integer = 8 conflicting, conflict_error := translatec.emit_package(inputs[:]) defer translatec.destroy_package_outputs(conflicting) defer delete(conflict_error) testing.expect_value(t, len(conflicting), 0) testing.expect(t, strings.contains(conflict_error, "conflicting generated C declaration 'SHARED_VALUE'")) } @(test) translate_c_package_cli_requires_unambiguous_outputs :: proc(t: ^testing.T) { testing.expect_value(t, run_translate_c([]string{ "brolang", "--translate-c", "first.h", "second.h", }), 2) testing.expect_value(t, run_translate_c([]string{ "brolang", "--translate-c", "a/same.h", "b/same.h", "--output-dir", "/tmp/unused", }), 2) } @(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(_ 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) _ = z _ = b _ = 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(_ 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(_ 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(_ u16) void {} take_f32 func(_ 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) } @(test) missing_qualified_signature_symbol_reports_one_root_error :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-root-diagnostic" defer _ = os2.remove_all(directory) _ = os2.remove_all(directory) testing.expect(t, os.make_directory(directory) == nil) token_text := `Token :: struct { start int } ` lexer_text := `scan func(cursor usize) void ! missing.Error { token Token = Token{start = cursor} _ = token } ` main_text := `main func() void { scan(1) catch |_| { return } } ` testing.expect(t, os.write_entire_file( "/tmp/brolang-test-root-diagnostic/token.bro", transmute([]byte)token_text, )) testing.expect(t, os.write_entire_file( "/tmp/brolang-test-root-diagnostic/lexer.bro", transmute([]byte)lexer_text, )) testing.expect(t, os.write_entire_file( "/tmp/brolang-test-root-diagnostic/main.bro", transmute([]byte)main_text, )) 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(directory, &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) testing.expect_value(t, len(diagnostics.items), 1) testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'") formatted := source.format(&diagnostics, source.Diagnostic_Id(0)) defer delete(formatted) testing.expect(t, strings.contains(formatted, "/lexer.bro:1:32")) testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol")) testing.expect(t, !strings.contains(formatted, "fallible expression")) testing.expect(t, !strings.contains(formatted, "must be consumed")) testing.expect(t, !strings.contains(formatted, "could not resolve the 'int' constraint")) } @(test) poisoned_global_and_local_types_do_not_create_inference_fallbacks :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-poisoned-declarations" defer _ = os2.remove_all(directory) _ = os2.remove_all(directory) testing.expect(t, os.make_directory(directory) == nil) text := `bad missing.Global :: 1 main func() void { value absent.Local = 1 _ = value } ` testing.expect(t, os.write_entire_file( "/tmp/brolang-test-poisoned-declarations/main.bro", transmute([]byte)text, )) 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(directory, &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) testing.expect_value(t, len(diagnostics.items), 2) unknown_missing := false unknown_absent := false for diagnostic in diagnostics.items { unknown_missing = unknown_missing || diagnostic.message == "unknown symbol 'missing'" unknown_absent = unknown_absent || diagnostic.message == "unknown symbol 'absent'" testing.expect(t, !strings.contains(diagnostic.message, "could not resolve")) testing.expect(t, !strings.contains(diagnostic.message, "could not infer")) } testing.expect(t, unknown_missing) testing.expect(t, unknown_absent) } named_record_field_type :: proc( module: ^hir.Module, symbols: ^symbol.Table, record_name, field_name: string, ) -> (types.Type, bool) { record := types.find_named(&module.types, 0, u32(symbol.intern(symbols, record_name))) if !types.is_record(record, &module.types) { return types.INVALID, false } field_symbol := symbol.intern(symbols, field_name) for field in types.fields_for(&module.types, record) { if field.name == u32(field_symbol) { return field.type, true } } return types.INVALID, false } @(test) native_record_constraint_fields_resolve_program_wide :: proc(t: ^testing.T) { text := `Token :: struct { start int } Backward :: struct { start int } Wide :: struct { value int } Literal :: struct { value int } Measurement :: struct { ratio float span range } Payload :: union { count int } take_usize func(value usize) usize { return value } exercise func(cursor usize, narrow i8, wider i16, count i32) i32 { token Token = Token{start = cursor} wide Wide = Wide{value = narrow} wide.value = wider small Literal = Literal{value = 1} large Literal = Literal{value = 1000} backward Backward = Backward{start = 1} measurement Measurement = Measurement{ratio = 1.5, span = 0..3} payload Payload = Payload{count = count} _ = token.start _ = wide.value _ = small.value _ = large.value _ = take_usize(backward.start) _ = measurement.ratio _ = measurement.span _ = payload.count return 0 } main func() i32 { return exercise(7, 1, 1000, 3) } ` source_file := source.Source{path="record_constraints.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) token_start, token_ok := named_record_field_type(&hir_module, &symbols, "Token", "start") backward_start, backward_ok := named_record_field_type(&hir_module, &symbols, "Backward", "start") wide_value, wide_ok := named_record_field_type(&hir_module, &symbols, "Wide", "value") literal_value, literal_ok := named_record_field_type(&hir_module, &symbols, "Literal", "value") ratio, ratio_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "ratio") span, span_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "span") count, count_ok := named_record_field_type(&hir_module, &symbols, "Payload", "count") testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, token_ok && backward_ok && wide_ok && literal_ok && ratio_ok && span_ok && count_ok) testing.expect_value(t, token_start, types.USIZE) testing.expect_value(t, backward_start, types.USIZE) testing.expect_value(t, wide_value, types.I16) testing.expect_value(t, literal_value, types.I16) testing.expect_value(t, ratio, types.F64) testing.expect(t, types.is_range(span, &hir_module.types)) testing.expect_value(t, types.child_type(span, &hir_module.types), types.I8) testing.expect_value(t, count, types.I32) } @(test) native_record_int_field_compiles_and_runs_as_usize :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-record-field-usize" main_path := "/tmp/brolang-test-record-field-usize/main.bro" output := "/tmp/brolang-test-record-field-usize-output" text := `Token :: struct { start int } main func() i32 { cursor usize = 7 token Token = Token{start = cursor} if (token.start != cursor) return 1 return 0 } ` _ = 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)) testing.expect_value(t, compiler_core.compile_package(directory, output), 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) native_record_constraint_fields_report_unresolved_and_conflicting_demands :: proc(t: ^testing.T) { cases := [2]struct { text: string, needle: string, }{ { text = `Token :: struct { start int } main func() void {} `, needle = "could not resolve the 'int' constraint for field 'Token.start'", }, { text = `Token :: struct { start int } use func(signed i32, unsigned usize) void { a Token = Token{start = signed} b Token = Token{start = unsigned} _ = a _ = b } main func() void { use(1, 2) } `, needle = "conflicting types i32 and usize for field 'Token.start' declared as 'int'", }, } for test_case in cases { source_file := source.Source{path="bad_record_constraint.bro", text=test_case.text} diagnostics := source.init_diagnostics(&source_file) symbols := symbol.init_table() stream := lexer.lex(&source_file, &diagnostics, &symbols) ast_module := parser.parse(&stream, &source_file, &diagnostics) hir_module := checker.check(&ast_module, &diagnostics, &symbols) ir_module := lower.lower(&hir_module) llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) matching := 0 for diagnostic in diagnostics.items { matching += 1 if strings.contains(diagnostic.message, test_case.needle) else 0 } testing.expect_value(t, matching, 1) testing.expect(t, len(llvm_text) > 0) delete(llvm_text) ir.destroy_module(&ir_module) hir.destroy_module(&hir_module) ast.destroy_module(&ast_module) delete(stream.items) symbol.destroy_table(&symbols) source.destroy_diagnostics(&diagnostics) } directory := "/tmp/brolang-test-record-field-conflict" main_path := "/tmp/brolang-test-record-field-conflict/main.bro" output := "/tmp/brolang-test-record-field-conflict-output" _ = 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)cases[1].text)) testing.expect_value(t, compiler_core.compile_package(directory, output), 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) constraint_fields_remain_rejected_outside_named_native_records :: proc(t: ^testing.T) { text := `BadC :: c_struct { value int } BadNested :: struct { values []int } make_type func() type { return struct { value int } } main func() void { Generated :: @make_type() _ = Generated } ` source_file := source.Source{path="excluded_record_constraints.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) record_errors := 0 anonymous_error := false for diagnostic in diagnostics.items { record_errors += 1 if strings.contains(diagnostic.message, "record fields must have runtime value types") else 0 anonymous_error = anonymous_error || strings.contains(diagnostic.message, "anonymous struct field 'value' requires a concrete runtime type") } c_field, c_ok := named_record_field_type(&hir_module, &symbols, "BadC", "value") nested_field, nested_ok := named_record_field_type(&hir_module, &symbols, "BadNested", "values") testing.expect_value(t, record_errors, 1) testing.expect(t, anonymous_error) testing.expect(t, c_ok && nested_ok) testing.expect_value(t, c_field, types.INT) testing.expect(t, types.is_slice(nested_field, &hir_module.types)) testing.expect_value(t, types.child_type(nested_field, &hir_module.types), types.INT) } @(test) parser_records_native_tests_and_test_imports :: proc(t: ^testing.T) { text := `test import "../math" addition test { return } ` source_file := source.Source{path="tests.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), 1) testing.expect(t, module.imports[0].test_only) testing.expect_value(t, module.imports[0].alias, symbol.INVALID) testing.expect_value(t, len(module.functions), 1) testing.expect(t, module.functions[0].test) testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "addition") } @(test) native_test_framework_discovers_reports_and_preserves_main :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-native-framework" root := "/tmp/brolang-test-native-framework/root" dependency := "/tmp/brolang-test-native-framework/dependency" root_path := "/tmp/brolang-test-native-framework/root/main.bro" dependency_path := "/tmp/brolang-test-native-framework/dependency/math.bro" test_output := "/tmp/brolang-test-native-framework-tests" app_output := "/tmp/brolang-test-native-framework-app" _ = os2.remove_all(directory) defer _ = os2.remove_all(directory) defer _ = os.remove(test_output) defer _ = os.remove(app_output) testing.expect(t, os2.make_directory_all(root) == nil) testing.expect(t, os2.make_directory_all(dependency) == nil) root_text := `testing :: import "@std/testing" test import "../dependency" main func() i32 { return 77 } root_passes test { try testing.expect(true) } root_fails test { try testing.expect_equal(42, 41) } root_continues test { try testing.expect(true) } ` dependency_text := `testing :: import "@std/testing" dependency_passes test { try testing.expect(true) } ` testing.expect(t, os.write_entire_file(root_path, transmute([]byte)root_text)) testing.expect(t, os.write_entire_file(dependency_path, transmute([]byte)dependency_text)) app_status := compiler_core.compile_package( root, app_output, nil, target.DEFAULT, cimport.Options{}, ".", ) testing.expect_value(t, app_status, 0) app_state := run_executable(app_output) testing.expect_value(t, app_state.exit_code, 77) test_status := compiler_core.compile_package( root, test_output, nil, target.DEFAULT, cimport.Options{}, ".", .Test, ) testing.expect_value(t, test_status, 0) state, stdout, stderr, _ := os2.process_exec( os2.Process_Desc{command=[]string{test_output}}, context.allocator, ) defer delete(stdout) defer delete(stderr) output := string(stderr) testing.expect_value(t, state.exit_code, 1) testing.expect(t, strings.contains(output, "PASS root.root_passes")) testing.expect(t, strings.contains(output, "FAIL root.root_fails")) testing.expect(t, strings.contains(output, "expected 42, found 41")) testing.expect(t, strings.contains(output, "PASS root.root_continues")) testing.expect(t, strings.contains(output, "PASS dependency.dependency_passes")) testing.expect(t, strings.contains(output, root_path)) testing.expect(t, strings.contains(output, "3 passed, 1 failed")) }