package main import compiler_core "./compiler" import "./compiler/ast" import "./compiler/backend" 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/token" import "./compiler/types" import "core:fmt" 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, i64(42)) testing.expect_value(t, module.statements[module.functions[0].body[0]].name, sink_symbol) } @(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_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_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 tok.span.start < len(text) && text[tok.span.start: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) 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, i64(3)) } 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.. (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) } } @(test) cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T) { options, valid := parse_cli_args([]string{ "brolang", "app", "--link", "native.c", "-o", "app.out", "--library-path", "vendor/lib", "--library", "thing", "--link", "helper.o", }) defer delete(options.link_arguments) testing.expect(t, valid) testing.expect_value(t, options.input_path, "app") testing.expect_value(t, options.output_path, "app.out") testing.expect_value(t, len(options.link_arguments), 4) testing.expect_value(t, options.link_arguments[0].kind, linker.Kind.Input) testing.expect_value(t, options.link_arguments[0].value, "native.c") testing.expect_value(t, options.link_arguments[1].kind, linker.Kind.Library_Path) testing.expect_value(t, options.link_arguments[2].kind, linker.Kind.Library) testing.expect_value(t, options.link_arguments[3].value, "helper.o") _, unknown_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--unknown", "value"}) _, incomplete_valid := parse_cli_args([]string{"brolang", "app", "-o"}) _, duplicate_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "one", "-o", "two"}) _, duplicate_empty_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "", "-o", "two"}) testing.expect(t, !unknown_valid) testing.expect(t, !incomplete_valid) testing.expect(t, !duplicate_output_valid) testing.expect(t, !duplicate_empty_output_valid) } @(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_rejects_chained_package_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(t, len(diagnostics.items) > 0) } @(test) lexer_diagnoses_invalid_import_strings :: proc(t: ^testing.T) { text := "import \"bad\\q\"\nimport \"unterminated\n" source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) testing.expect_value(t, len(diagnostics.items), 2) } @(test) package_loader_discovers_lexical_immediate_bro_files :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) module, loaded := loader.load("examples/packages/basic/app", &sources, &diagnostics, &symbols) defer ast.destroy_module(&module) testing.expect(t, loaded) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(module.packages), 2) testing.expect_value(t, len(module.files), 3) testing.expect(t, strings.has_suffix(sources.items[module.files[0].source].path, "/main.bro")) testing.expect(t, strings.has_suffix(sources.items[module.files[1].source].path, "/value.bro")) testing.expect(t, strings.has_suffix(sources.items[module.files[2].source].path, "/math.bro")) } @(test) multi_source_diagnostics_report_the_originating_file :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) module, loaded := loader.load("examples/packages/file_local/app", &sources, &diagnostics, &symbols) defer ast.destroy_module(&module) hir_module := checker.check(&module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) testing.expect(t, loaded) found := false for _, diagnostic_id in diagnostics.items { message := source.format(&diagnostics, diagnostic_id) if strings.contains(message, "/b.bro:2:9:") && strings.contains(message, "unknown package alias 'math'") { found = true } delete(message) } testing.expect(t, found) } @(test) semantic_lookup_diagnoses_wrong_declaration_kinds :: proc(t: ^testing.T) { text := `value :: 1 give :: func() i8 { return 1 } main :: func() void { _ = value() _ = give } ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) found_global := false found_function := false for diagnostic in diagnostics.items { found_global = found_global || strings.contains(diagnostic.message, "'value' is a global, not a function") found_function = found_function || strings.contains(diagnostic.message, "'give' is a function, not a global value") } testing.expect(t, found_global) testing.expect(t, found_function) } @(test) pipeline_emits_specialized_calling_conventions_and_checked_add :: proc(t: ^testing.T) { text := `sum_c :: c func(a, b int) int { return a + b } sum_bro :: func(a, b int) int { return a + b } main :: func() void { _ = sum_c(1, 2) _ = sum_bro(1, 2) } ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) ir_module := lower.lower(&hir_module) defer ir.destroy_module(&ir_module) llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) defer delete(llvm_text) second_llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) defer delete(second_llvm_text) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, llvm_text, second_llvm_text) testing.expect(t, strings.contains(llvm_text, "define 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) invalid_foreign_declarations_are_eagerly_diagnosed_and_calls_trap :: proc(t: ^testing.T) { text := `bad :: c func(value int) int native :: func() i32 main :: func() void { _ = bad(1) } ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) ir_module := lower.lower(&hir_module) defer ir.destroy_module(&ir_module) llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) defer delete(llvm_text) found_parameter := false found_result := false found_native := false for diagnostic in diagnostics.items { found_parameter = found_parameter || strings.contains(diagnostic.message, "requires concrete parameter types") found_result = found_result || strings.contains(diagnostic.message, "requires a concrete or void result type") found_native = found_native || strings.contains(diagnostic.message, "must use 'c func'") } testing.expect(t, found_parameter) testing.expect(t, found_result) testing.expect(t, found_native) testing.expect(t, !strings.contains(llvm_text, "@bad(")) testing.expect(t, strings.contains(llvm_text, "call void @bro.trap")) } @(test) duplicate_foreign_symbols_across_packages_are_poisoned :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) ast_module, loaded := loader.load("examples/packages/foreign_duplicate/app", &sources, &diagnostics, &symbols) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) ir_module := lower.lower(&hir_module) defer ir.destroy_module(&ir_module) llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) defer delete(llvm_text) duplicate_count := 0 for diagnostic in diagnostics.items { if strings.contains(diagnostic.message, "duplicate foreign symbol 'same'") { duplicate_count += 1 } } testing.expect(t, loaded) testing.expect_value(t, duplicate_count, 2) testing.expect(t, !strings.contains(llvm_text, "@same(")) testing.expect(t, strings.contains(llvm_text, "call void @bro.trap")) } @(test) bodyless_root_main_recovers_as_a_trap_definition :: proc(t: ^testing.T) { text := "main :: c func() i32\n" source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) ir_module := lower.lower(&hir_module) defer ir.destroy_module(&ir_module) llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) defer delete(llvm_text) found := false for diagnostic in diagnostics.items { found = found || strings.contains(diagnostic.message, "main must have a body") } testing.expect(t, found) testing.expect_value(t, len(ir_module.functions), 1) testing.expect_value(t, ir_module.functions[0].implementation, ir.Implementation.Definition) testing.expect(t, strings.contains(llvm_text, "define i32 @main()")) testing.expect(t, !strings.contains(llvm_text, "declare i32 @main()")) } @(test) literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) { text := `return_i16 :: func() i16 { return 1 + 2 } take_i16 :: func(value i16) i16 { return value } take_int :: func(value int) int { return value } main :: func() void { local i16 :: 1 + 2 _ = 100 + (20 + 8) _ = return_i16() _ = take_i16(1 + 2) _ = take_int(127 + 1) } ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) ir_module := lower.lower(&hir_module) defer ir.destroy_module(&ir_module) llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) defer delete(llvm_text) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, strings.contains(llvm_text, "@bro__p0__take_int__i16")) for function in ir_module.functions { for instruction in function.instructions { testing.expect(t, instruction.op != ir.Opcode.Add_Checked) } } for function in hir_module.functions { for statement_id in function.body { statement := hir_module.statements[statement_id] if statement.expr < 0 { continue } expr := hir_module.exprs[statement.expr] if expr.kind == .Integer { testing.expect(t, types.equal(expr.type, types.I16)) } if expr.kind == .Call && symbol.resolve(&symbols, function.name) == "main" && len(expr.args) > 0 { arg := hir_module.exprs[expr.args[0]] testing.expect_value(t, arg.kind, hir.Expr_Kind.Integer) testing.expect(t, types.equal(arg.type, types.I16)) } } } } @(test) runtime_arithmetic_does_not_inherit_result_context :: proc(t: ^testing.T) { text := `widen_after_add :: func(value i8) i16 { return value + 1 } main :: func() void { _ = widen_after_add(1) } ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) testing.expect_value(t, len(diagnostics.items), 0) found := false for function in hir_module.functions { if symbol.resolve(&symbols, function.name) != "widen_after_add" { continue } found = true statement := hir_module.statements[function.body[0]] widen := hir_module.exprs[statement.expr] add := hir_module.exprs[widen.left] testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen) testing.expect(t, types.equal(widen.type, types.I16)) testing.expect_value(t, add.kind, hir.Expr_Kind.Add) testing.expect(t, types.equal(add.type, types.I8)) } testing.expect(t, found) } @(test) parser_recovers_after_invalid_tokens :: proc(t: ^testing.T) { text := `broken @ declaration main :: func() void {} ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&module) testing.expect(t, len(diagnostics.items) > 0) testing.expect_value(t, len(module.functions), 1) testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "main") } @(test) return_sink_and_unconsumed_values_have_distinct_hir :: proc(t: ^testing.T) { text := `give :: func() i8 { return 1 } done :: func() void { return _ } main :: func() void { done() _ = give() give() } ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) done_id, main_id := -1, -1 for function, id in hir_module.functions { if symbol.resolve(&symbols, function.name) == "done" { done_id = id } else if symbol.resolve(&symbols, function.name) == "main" { main_id = id } } testing.expect(t, done_id >= 0) testing.expect(t, main_id >= 0) testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].kind, hir.Stmt_Kind.Return) testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].expr, -1) main := hir_module.functions[main_id] testing.expect_value(t, hir_module.statements[main.body[0]].kind, hir.Stmt_Kind.Expression) testing.expect_value(t, hir_module.statements[main.body[1]].kind, hir.Stmt_Kind.Sink) testing.expect_value(t, hir_module.statements[main.body[2]].kind, hir.Stmt_Kind.Trap) } @(test) recursive_specialization_reaches_a_fixed_point :: proc(t: ^testing.T) { text := `a :: func(value int) i32 { return b(value) } b :: func(value int) i32 { return a(value) } main :: func() void { _ = a(1) } ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(hir_module.functions), 3) } @(test) 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, "f%d :: func(value int) int ", index) strings.write_string(&builder, "{ return ") if index == 69 { strings.write_string(&builder, "value") } else { fmt.sbprintf(&builder, "f%d(value)", index+1) } strings.write_string(&builder, " }\n") } strings.write_string(&builder, "main :: func() i32 { return f0(1) }\n") source_file := source.Source{path="test.bro", text=strings.to_string(builder)} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(hir_module.functions), 71) } @(test) globals_and_generic_results_reach_a_shared_fixed_point :: proc(t: ^testing.T) { text := `derived :: identity(base) base :: make() identity :: func(value int) int { return value } make :: func() int { return 1 } main :: func() i32 { return derived } ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(hir_module.functions), 3) for global in hir_module.globals { testing.expect(t, types.equal(global.type, types.I8)) } } @(test) unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) { text := `broken :: func(value, value i8, nope void) void {} main :: func() void {} ` source_file := source.Source{path="test.bro", text=text} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) found_duplicate := false found_void := false for diagnostic in diagnostics.items { found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate parameter 'value'") found_void = found_void || strings.contains(diagnostic.message, "void is only valid as a function result type") } testing.expect(t, found_duplicate) testing.expect(t, found_void) } run_command_success :: proc(command: []string) -> bool { state, stdout, stderr, err := os2.process_exec( os2.Process_Desc{command=command}, context.allocator, ) delete(stdout) delete(stderr) return err == nil && state.exit_code == 0 } prepare_native_object :: proc(output: string) -> bool { local_cache := fmt.aprintf("ZIG_LOCAL_CACHE_DIR=%s-zig-cache", output) defer delete(local_cache) global_cache := fmt.aprintf("ZIG_GLOBAL_CACHE_DIR=%s-zig-global-cache", output) defer delete(global_cache) return run_command_success([]string{ "/usr/bin/env", local_cache, global_cache, "zig", "cc", "-c", "examples/interop/manual/native.c", "-o", output, }) } prepare_native_archive :: proc(object, archive: string) -> bool { return run_command_success([]string{ "/usr/bin/ar", "-rcs", archive, object, }) } run_executable :: proc(path: string) -> os2.Process_State { state, stdout, stderr, _ := os2.process_exec( os2.Process_Desc{command=[]string{path}}, context.allocator, ) delete(stdout) delete(stderr) return state } @(test) valid_program_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-valid" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/prototype", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) 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) foreign_function_links_from_object :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-object" object := "/tmp/brolang-test-foreign-object.o" defer _ = os.remove(output) defer _ = os.remove(object) testing.expect(t, prepare_native_object(object)) arguments := []linker.Argument{{kind=.Input, value=object}} status := compiler_core.compile_package("examples/interop/manual", output, arguments) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 42) } @(test) foreign_function_links_from_direct_library :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-direct-library" object := "/tmp/brolang-test-foreign-direct-library.o" archive := "/tmp/brolang-test-foreign-direct-library.a" defer _ = os.remove(output) defer _ = os.remove(object) defer _ = os.remove(archive) testing.expect(t, prepare_native_object(object)) testing.expect(t, prepare_native_archive(object, archive)) arguments := []linker.Argument{{kind=.Input, value=archive}} status := compiler_core.compile_package("examples/interop/manual", output, arguments) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 42) } @(test) foreign_function_links_from_named_library :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-named-library" object := "/tmp/brolang-test-foreign-named-library.o" archive := "/tmp/libbrolang-test-foreign-named.a" defer _ = os.remove(output) defer _ = os.remove(object) defer _ = os.remove(archive) testing.expect(t, prepare_native_object(object)) testing.expect(t, prepare_native_archive(object, archive)) arguments := []linker.Argument{ {kind=.Library_Path, value="/tmp"}, {kind=.Library, value="brolang-test-foreign-named"}, } status := compiler_core.compile_package("examples/interop/manual", output, arguments) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 42) } @(test) one_line_main_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-one-line-main" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/one_line", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 7) } @(test) folded_constant_addition_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-constant-fold" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/constant_fold", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) unused_invalid_global_does_not_trap :: proc(t: ^testing.T) { output := "/tmp/brolang-test-invalid-unused" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/invalid_unused_global", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) used_invalid_global_traps :: proc(t: ^testing.T) { output := "/tmp/brolang-test-invalid-used" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/invalid_used_global", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect(t, !state.success) } @(test) main_int_is_constrained_to_i32 :: proc(t: ^testing.T) { output := "/tmp/brolang-test-main-int" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/main_int", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 3) } @(test) main_i32_returns_directly :: proc(t: ^testing.T) { output := "/tmp/brolang-test-main-i32" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/main_i32", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect_value(t, state.exit_code, 4) } @(test) transitive_problematic_global_is_deferred :: proc(t: ^testing.T) { output := "/tmp/brolang-test-transitive-unused" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/invalid_transitive_unused_global", output) testing.expect_value(t, status, 1) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(test) checked_addition_traps_on_overflow :: proc(t: ^testing.T) { output := "/tmp/brolang-test-overflow" defer _ = os.remove(output) status := compiler_core.compile_package("examples/programs/overflow", output) testing.expect_value(t, status, 0) state := run_executable(output) testing.expect(t, !state.success) } @(test) 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"}, } command := backend.build_command("module.ll", "program", arguments) defer backend.destroy_command(command) expected := []string{ "/usr/bin/env", "zig", "cc", "-Wno-override-module", "module.ll", "native.c", "-Lvendor/lib", "-lthing", "helper.o", "-o", "program", } testing.expect_value(t, len(command), len(expected)) for value, index in expected { testing.expect_value(t, command[index], value) } } @(test) source_store_owns_buffers_indexes_lines_and_deduplicates_diagnostics :: proc(t: ^testing.T) { store := source.init_store() defer source.destroy_store(&store) bytes := make([]byte, len("one\ntwo\n")) copy(bytes, "one\ntwo\n") source_id := source.add_source_owned(&store, "owned.bro", bytes) bytes[0] = 'O' diagnostics := source.init_store_diagnostics(&store) defer source.destroy_diagnostics(&diagnostics) span := source.Span{file=source_id, start=4, end=7} first := source.add(&diagnostics, span, "same") second := source.addf(&diagnostics, span, "%s", "same") other := source.add(&diagnostics, span, "other") formatted := source.format(&diagnostics, other) defer delete(formatted) testing.expect_value(t, store.items[source_id].text, "One\ntwo\n") testing.expect_value(t, len(store.items[source_id].line_starts), 3) testing.expect_value(t, first, second) testing.expect_value(t, len(diagnostics.items), 2) testing.expect(t, strings.contains(formatted, "owned.bro:2:1:")) } @(test) maximum_signed_i64_literal_parses_exactly :: proc(t: ^testing.T) { source_file := source.Source{path="test.bro", text="value :: 9223372036854775807\nmain :: func() void {}\n"} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&module) testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, module.exprs[module.globals[0].expr].integer, i64(9223372036854775807)) } @(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, 3) instructions[0] = ir.Instruction{op=.Store, type=types.I8, a=-1, b=-1, diagnostic=-1} instructions[1] = ir.Instruction{op=.Add_Checked, type=types.I32, a=-1, b=-1, diagnostic=-1} instructions[2] = ir.Instruction{op=.Return, type=types.I32, a=1, b=-1, diagnostic=-1} 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, "@bro.trap(ptr %message, i64 %length) noreturn")) testing.expect(t, !strings.contains(text, "%v-1")) llvm_path := "/tmp/brolang-test-malformed-recovery.ll" output := "/tmp/brolang-test-malformed-recovery" defer _ = os.remove(llvm_path) defer _ = os.remove(output) testing.expect(t, os.write_entire_file(llvm_path, transmute([]byte)text)) testing.expect(t, backend.compile(llvm_path, output)) } @(test) hundred_thousand_term_runtime_addition_uses_iterative_pipeline :: proc(t: ^testing.T) { builder := strings.builder_make() defer strings.builder_destroy(&builder) strings.write_string(&builder, "sum :: func(value i32) i32 { return value") for _ in 0..<100_000 { strings.write_string(&builder, " + 1") } strings.write_string(&builder, " }\nmain :: func() void { _ = sum(0) }\n") source_file := source.Source{path="test.bro", text=strings.to_string(builder)} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) stream := lexer.lex(&source_file, &diagnostics, &symbols) defer delete(stream.items) ast_module := parser.parse(&stream, &source_file, &diagnostics) defer ast.destroy_module(&ast_module) hir_module := checker.check(&ast_module, &diagnostics, &symbols) defer hir.destroy_module(&hir_module) ir_module := lower.lower(&hir_module) defer ir.destroy_module(&ir_module) instruction_count := 0 for function in ir_module.functions { instruction_count += len(function.instructions) } testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, instruction_count > 100_000) } @(test) deep_global_cycle_detection_uses_iterative_dfs :: proc(t: ^testing.T) { count := 50_000 ast_module := ast.init_module() defer ast.destroy_module(&ast_module) source_file := source.Source{path="test.bro", text=""} diagnostics := source.init_diagnostics(&source_file) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() defer symbol.destroy_table(&symbols) name := symbol.intern(&symbols, "value") state := checker.Checker{ ast_module=&ast_module, diagnostics=&diagnostics, symbols=&symbols, module=hir.init_module(), allocator=context.allocator, } defer hir.destroy_module(&state.module) defer delete(state.cycle_stack) for id in 0..