Files
brolang/compiler_tests.odin
T
2026-06-23 18:24:36 +02:00

5653 lines
195 KiB
Odin

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/types"
import "core:fmt"
import "core:mem"
import "core:os"
import "core:os/os2"
import "core:strings"
import "core:testing"
@(test)
symbol_table_deduplicates_and_owns_spellings :: proc(t: ^testing.T) {
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
buffer := [5]byte{'a', 'l', 'p', 'h', 'a'}
alpha := symbol.intern(&symbols, string(buffer[:]))
duplicate := symbol.intern(&symbols, "alpha")
beta := symbol.intern(&symbols, "beta")
buffer[0] = 'x'
testing.expect_value(t, alpha, duplicate)
testing.expect(t, alpha != beta)
testing.expect_value(t, symbol.resolve(&symbols, alpha), "alpha")
testing.expect_value(t, symbol.resolve(&symbols, beta), "beta")
testing.expect_value(t, symbol.intern(&symbols, ""), symbol.INVALID)
testing.expect_value(t, symbol.resolve(&symbols, symbol.INVALID), "")
testing.expect(t, !symbol.is_valid(symbol.INVALID))
testing.expect(t, symbol.is_valid(alpha))
}
@(test)
compact_tokens_intern_only_identifiers_and_preserve_parser_text :: proc(t: ^testing.T) {
text := `other :: import "../math"
value :: 42
main :: func() void { _ = value }
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
value_symbol := symbol.intern(&symbols, "value")
sink_symbol := symbol.intern(&symbols, "_")
value_count := 0
for tok in stream.items {
#partial switch tok.kind {
case .Identifier:
if tok.symbol == value_symbol {
value_count += 1
}
case .Underscore:
testing.expect_value(t, tok.symbol, sink_symbol)
case:
testing.expect_value(t, tok.symbol, symbol.INVALID)
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, value_count, 2)
testing.expect_value(t, module.imports[0].path, "../math")
testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(42))
testing.expect_value(t, module.statements[module.functions[0].body[0]].name, sink_symbol)
}
@(test)
compact_ids_reserve_invalid_values_and_preserve_layout :: proc(t: ^testing.T) {
testing.expect_value(t, size_of(source.Span), 12)
testing.expect_value(t, size_of(token.Token), 24)
testing.expect(t, size_of(ast.Expr) <= 64)
testing.expect(t, size_of(hir.Expr) <= 88)
testing.expect(t, size_of(ir.Instruction) <= 88)
testing.expect_value(t, size_of(types.Type), 4)
source_index, source_ok := source.source_index(source.Source_Id(0), 1)
testing.expect_value(t, source_index, 0)
testing.expect(t, source_ok)
_, source_invalid := source.source_index(source.INVALID_SOURCE, 1)
testing.expect(t, !source_invalid)
_, source_out_of_bounds := source.source_index(source.Source_Id(1), 1)
testing.expect(t, !source_out_of_bounds)
diagnostic_index, diagnostic_ok := source.diagnostic_index(source.Diagnostic_Id(0), 1)
testing.expect_value(t, diagnostic_index, 0)
testing.expect(t, diagnostic_ok)
_, diagnostic_invalid := source.diagnostic_index(source.INVALID_DIAGNOSTIC, 1)
testing.expect(t, !diagnostic_invalid)
expr_index, expr_ok := ast.index(ast.Expr_Id(0), ast.INVALID_EXPR, 1)
testing.expect_value(t, expr_index, 0)
testing.expect(t, expr_ok)
_, expr_invalid := ast.index(ast.INVALID_EXPR, ast.INVALID_EXPR, 1)
testing.expect(t, !expr_invalid)
function_index, function_ok := hir.index(hir.Function_Id(0), hir.INVALID_FUNCTION, 1)
testing.expect_value(t, function_index, 0)
testing.expect(t, function_ok)
_, function_invalid := hir.index(hir.INVALID_FUNCTION, hir.INVALID_FUNCTION, 1)
testing.expect(t, !function_invalid)
instruction_index, instruction_ok := ir.index(ir.Instruction_Id(0), ir.INVALID_INSTRUCTION, 1)
testing.expect_value(t, instruction_index, 0)
testing.expect(t, instruction_ok)
_, instruction_invalid := ir.index(ir.INVALID_INSTRUCTION, ir.INVALID_INSTRUCTION, 1)
testing.expect(t, !instruction_invalid)
spec_index, spec_ok := checker.spec_index(checker.Spec_Id(0), 1)
testing.expect_value(t, spec_index, 0)
testing.expect(t, spec_ok)
_, spec_invalid := checker.spec_index(checker.INVALID_SPEC, 1)
testing.expect(t, !spec_invalid)
testing.expect(t, source.fits_source_length(u64(0xffff_ffff)))
testing.expect(t, !source.fits_source_length(u64(0x1_0000_0000)))
}
@(test)
lexer_preserves_newlines_and_skips_comments :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="# comment\nmain :: func() void {}\n"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, stream.items[0].kind, token.Kind.Newline)
testing.expect_value(t, stream.items[1].kind, token.Kind.Identifier)
}
@(test)
parser_accepts_grouped_params_and_multiline_statements :: proc(t: ^testing.T) {
text := `sum :: func(a,
b int) int {
return (a
+ b)
}
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.functions), 2)
testing.expect_value(t, len(module.functions[0].params), 2)
}
@(test)
parser_accepts_sentinel_many_item_pointer_types :: proc(t: ^testing.T) {
text := `zero :: func(value [*;0]u8) void {}
newline :: func(value [*;'\n']mut u8) void {}
nullable :: func(value ?[*;0]u8) void {}
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
zero, zero_ok := types.node(&module.type_store, module.functions[0].params[0].type)
newline, newline_ok := types.node(&module.type_store, module.functions[1].params[0].type)
nullable, nullable_ok := types.node(&module.type_store, module.functions[2].params[0].type)
nullable_child, nullable_child_ok := types.node(&module.type_store, nullable.child)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, zero_ok && zero.kind == .Pointer && zero.many && zero.has_sentinel && zero.sentinel == 0)
testing.expect(t, newline_ok && newline.kind == .Pointer && newline.many && newline.mutable &&
newline.has_sentinel && newline.sentinel == '\n')
testing.expect(t, nullable_ok && nullable.kind == .Optional)
testing.expect(t, nullable_child_ok && nullable_child.kind == .Pointer &&
nullable_child.many && nullable_child.has_sentinel)
}
@(test)
parser_accepts_c_function_pointer_types :: proc(t: ^testing.T) {
text := `take :: c_func(callback ?*c_func(value c_int) c_int) void
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
optional, optional_ok := types.node(&module.type_store, module.functions[0].params[0].type)
pointer, pointer_ok := types.node(&module.type_store, optional.child)
function, function_ok := types.node(&module.type_store, pointer.child)
params := types.params_for(&module.type_store, pointer.child)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, optional_ok && optional.kind == .Optional)
testing.expect(t, pointer_ok && pointer.kind == .Pointer && pointer.many && !pointer.mutable)
testing.expect(t, function_ok && function.kind == .Function && function.c_abi && !function.variadic)
testing.expect(t, function.child == types.C_INT)
testing.expect_value(t, len(params), 1)
testing.expect(t, params[0].type == types.C_INT)
}
@(test)
parser_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)
pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="value :: 1 + 2 + 3\nmain :: func() void {}\n"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
root := module.exprs[module.globals[0].expr]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, root.kind, ast.Expr_Kind.Add)
testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Add)
testing.expect_value(t, module.exprs[root.right].integer, u64(3))
}
@(test)
pratt_parser_handles_prefix_negation_precedence :: proc(t: ^testing.T) {
text := `identity :: func(value i8) i8 { return value }
loose :: -1 + 2
grouped :: -(1 + 2)
called :: -identity(1)
chained :: --1
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
loose := module.exprs[module.globals[0].expr]
grouped := module.exprs[module.globals[1].expr]
called := module.exprs[module.globals[2].expr]
chained := module.exprs[module.globals[3].expr]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, loose.kind, ast.Expr_Kind.Add)
testing.expect_value(t, module.exprs[loose.left].kind, ast.Expr_Kind.Negate)
testing.expect_value(t, grouped.kind, ast.Expr_Kind.Negate)
testing.expect_value(t, module.exprs[grouped.left].kind, ast.Expr_Kind.Add)
testing.expect_value(t, called.kind, ast.Expr_Kind.Negate)
testing.expect_value(t, module.exprs[called.left].kind, ast.Expr_Kind.Call)
testing.expect_value(t, chained.kind, ast.Expr_Kind.Negate)
testing.expect_value(t, module.exprs[chained.left].kind, ast.Expr_Kind.Negate)
}
nested_expression_source :: proc(call: bool, depth: int) -> string {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
if call {
strings.write_string(&builder, "identity :: func(value i32) i32 { return value }\nvalue :: ")
for _ in 0..<depth {
strings.write_string(&builder, "identity(")
}
} else {
strings.write_string(&builder, "value :: ")
for _ in 0..<depth {
strings.write_byte(&builder, '(')
}
}
strings.write_byte(&builder, '1')
for _ in 0..<depth {
strings.write_byte(&builder, ')')
}
strings.write_string(&builder, "\nmain :: func() void {}\n")
return strings.clone(strings.to_string(builder))
}
nested_negation_source :: proc(depth: int) -> string {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
strings.write_string(&builder, "value :: ")
for _ in 0..<depth {
strings.write_byte(&builder, '-')
}
strings.write_string(&builder, "1\nmain :: func() void {}\n")
return strings.clone(strings.to_string(builder))
}
parse_nesting_result :: proc(text: string) -> (count: int, found_budget: bool) {
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
for diagnostic in diagnostics.items {
found_budget = found_budget || strings.contains(diagnostic.message, "expression nesting exceeds 256 levels")
}
return len(diagnostics.items), found_budget
}
@(test)
parser_enforces_explicit_expression_nesting_budget :: proc(t: ^testing.T) {
modes := [?]bool{false, true}
for call in modes {
at_limit := nested_expression_source(call, parser.MAX_EXPRESSION_NESTING)
defer delete(at_limit)
count, found := parse_nesting_result(at_limit)
testing.expect_value(t, count, 0)
testing.expect(t, !found)
over_limit := nested_expression_source(call, parser.MAX_EXPRESSION_NESTING+1)
defer delete(over_limit)
_, found = parse_nesting_result(over_limit)
testing.expect(t, found)
}
at_limit := nested_negation_source(parser.MAX_EXPRESSION_NESTING)
defer delete(at_limit)
count, found := parse_nesting_result(at_limit)
testing.expect_value(t, count, 0)
testing.expect(t, !found)
over_limit := nested_negation_source(parser.MAX_EXPRESSION_NESTING+1)
defer delete(over_limit)
_, found = parse_nesting_result(over_limit)
testing.expect(t, found)
}
@(test)
cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T) {
options, valid := parse_cli_args([]string{
"brolang",
"app",
"--c-link",
"native.c",
"-o",
"app.out",
"--c-library-path",
"vendor/lib",
"--c-library",
"thing",
"--c-link",
"helper.o",
"--c-include-path",
"vendor/include",
"--c-define",
"FEATURE=1",
"--target",
"aarch64-macos",
})
defer delete(options.link_arguments)
defer delete(options.c_options.include_paths)
defer delete(options.c_options.defines)
testing.expect(t, valid)
testing.expect_value(t, options.input_path, "app")
testing.expect_value(t, options.output_path, "app.out")
testing.expect_value(t, len(options.link_arguments), 4)
testing.expect_value(t, options.link_arguments[0].kind, linker.Kind.Input)
testing.expect_value(t, options.link_arguments[0].value, "native.c")
testing.expect_value(t, options.link_arguments[1].kind, linker.Kind.Library_Path)
testing.expect_value(t, options.link_arguments[2].kind, linker.Kind.Library)
testing.expect_value(t, options.link_arguments[3].value, "helper.o")
testing.expect_value(t, options.c_options.include_paths[0], "vendor/include")
testing.expect_value(t, options.c_options.defines[0], "FEATURE=1")
testing.expect_value(t, target.name(options.target), "aarch64-macos")
_, unknown_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--unknown", "value"})
_, incomplete_valid := parse_cli_args([]string{"brolang", "app", "-o"})
_, duplicate_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "one", "-o", "two"})
_, duplicate_empty_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "", "-o", "two"})
_, invalid_target := parse_cli_args([]string{"brolang", "app", "-o", "out", "--target", "x86_64-linux"})
_, legacy_link := parse_cli_args([]string{"brolang", "app", "-o", "out", "--link", "native.c"})
_, legacy_library_path := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library-path", "vendor/lib"})
_, legacy_library := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library", "thing"})
testing.expect(t, !unknown_valid)
testing.expect(t, !incomplete_valid)
testing.expect(t, !duplicate_output_valid)
testing.expect(t, !duplicate_empty_output_valid)
testing.expect(t, !invalid_target)
testing.expect(t, !legacy_link)
testing.expect(t, !legacy_library_path)
testing.expect(t, !legacy_library)
}
@(test)
parser_accepts_bare_and_aliased_imports :: proc(t: ^testing.T) {
text := `import
"../math"
other :: import "../math"
escaped :: import "dir\"name\\tail"
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.imports), 3)
testing.expect_value(t, symbol.resolve(&symbols, module.imports[0].alias), "")
testing.expect_value(t, module.imports[0].path, "../math")
testing.expect_value(t, symbol.resolve(&symbols, module.imports[1].alias), "other")
testing.expect_value(t, module.imports[2].path, "dir\"name\\tail")
}
@(test)
parser_accepts_chained_field_access :: proc(t: ^testing.T) {
text := `main :: func() void {
_ = first.second.value
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
lexer_diagnoses_invalid_import_strings :: proc(t: ^testing.T) {
text := "import \"bad\\q\"\nimport \"unterminated\n"
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
testing.expect_value(t, len(diagnostics.items), 2)
}
@(test)
package_loader_discovers_lexical_immediate_bro_files :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load("examples/packages/basic/app", &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.packages), 2)
testing.expect_value(t, len(module.files), 3)
testing.expect(t, strings.has_suffix(sources.items[module.files[0].source].path, "/main.bro"))
testing.expect(t, strings.has_suffix(sources.items[module.files[1].source].path, "/value.bro"))
testing.expect(t, strings.has_suffix(sources.items[module.files[2].source].path, "/math.bro"))
}
@(test)
multi_source_diagnostics_report_the_originating_file :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load("examples/packages/file_local/app", &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
found := false
for _, diagnostic_index in diagnostics.items {
message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index))
if strings.contains(message, "/b.bro:2:9:") &&
strings.contains(message, "unknown package alias 'math'") {
found = true
}
delete(message)
}
testing.expect(t, found)
}
@(test)
semantic_lookup_diagnoses_wrong_declaration_kinds :: proc(t: ^testing.T) {
text := `value :: 1
give :: func() i8 {
return 1
}
main :: func() void {
_ = value()
_ = give
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_global := false
found_function := false
for diagnostic in diagnostics.items {
found_global = found_global || strings.contains(diagnostic.message, "'value' is a global, not a function")
found_function = found_function || strings.contains(diagnostic.message, "'give' is a function, not a global value")
}
testing.expect(t, found_global)
testing.expect(t, found_function)
}
@(test)
pipeline_emits_specialized_calling_conventions_and_checked_add :: proc(t: ^testing.T) {
text := `sum_c :: c_func(a, b int) int {
return a + b
}
sum_bro :: func(a, b int) int {
return a + b
}
main :: func() void {
_ = sum_c(1, 2)
_ = sum_bro(1, 2)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
second_llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(second_llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, llvm_text, second_llvm_text)
testing.expect(t, strings.contains(llvm_text, "define signext i8 @bro_c__p0__sum_c__i8__i8"))
testing.expect(t, strings.contains(llvm_text, "define internal fastcc i8 @bro__p0__sum_bro__i8__i8"))
testing.expect(t, strings.contains(llvm_text, "@llvm.sadd.with.overflow.i8"))
}
@(test)
pipeline_emits_only_referenced_foreign_declarations_with_exact_names :: proc(t: ^testing.T) {
text := `used :: c_func(a, b i32) i32
unused :: c_func() i32
bodyful :: c_func(value i32) i32 {
return value
}
main :: func() void {
_ = used(1, 2)
_ = bodyful(3)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "declare i32 @used(i32, i32)"))
testing.expect(t, strings.contains(llvm_text, "call i32 @used(i32 1, i32 2)"))
testing.expect(t, !strings.contains(llvm_text, "@unused("))
testing.expect(t, strings.contains(llvm_text, "define i32 @bro_c__p0__bodyful__i32"))
}
@(test)
c_primitives_remain_distinct_with_apple_silicon_representations :: proc(t: ^testing.T) {
testing.expect(t, types.C_CHAR != types.C_SCHAR)
testing.expect(t, types.C_SCHAR != types.C_UCHAR)
testing.expect(t, types.C_INT != types.I32)
testing.expect(t, types.C_ULONG != types.U64)
testing.expect_value(t, types.representation(types.C_CHAR), types.I8)
testing.expect_value(t, types.representation(types.C_SCHAR), types.I8)
testing.expect_value(t, types.representation(types.C_UCHAR), types.U8)
testing.expect_value(t, types.representation(types.C_SHORT), types.I16)
testing.expect_value(t, types.representation(types.C_USHORT), types.U16)
testing.expect_value(t, types.representation(types.C_INT), types.I32)
testing.expect_value(t, types.representation(types.C_UINT), types.U32)
testing.expect_value(t, types.representation(types.C_LONG), types.I64)
testing.expect_value(t, types.representation(types.C_ULONG), types.U64)
testing.expect_value(t, types.representation(types.C_LONGLONG), types.I64)
testing.expect_value(t, types.representation(types.C_ULONGLONG), types.U64)
testing.expect_value(t, types.representation(types.C_FLOAT), types.F32)
testing.expect_value(t, types.representation(types.C_DOUBLE), types.F64)
testing.expect_value(t, types.representation(types.C_LONGDOUBLE), types.F64)
testing.expect_value(t, types.c_vararg_promotion(types.I8), types.C_INT)
testing.expect_value(t, types.c_vararg_promotion(types.U16), types.C_INT)
testing.expect_value(t, types.c_vararg_promotion(types.C_CHAR), types.C_INT)
testing.expect_value(t, types.c_vararg_promotion(types.C_USHORT), types.C_INT)
testing.expect_value(t, types.c_vararg_promotion(types.F32), types.C_DOUBLE)
testing.expect_value(t, types.c_vararg_promotion(types.C_FLOAT), types.C_DOUBLE)
testing.expect_value(t, types.c_vararg_promotion(types.U32), types.U32)
testing.expect_value(t, types.c_vararg_promotion(types.C_DOUBLE), types.C_DOUBLE)
testing.expect_value(t, target.llvm_triple(target.DEFAULT), "arm64-apple-macosx13.0.0")
}
@(test)
interop_foundation_emits_compounds_and_narrow_c_abi_attributes :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
y i32
}
signed :: c_func(value c_char) c_char
unsigned :: c_func(value c_uchar) c_uchar
exact :: c_func(value u32) u32
fallback :: func() i32 {
return 9
}
main :: func() void {
c :: 1
values [2;0]mut u8 = [1, 2]
point Point :: Point{x = 3, y = 4}
maybe ?i32 = 5
_ = c
_ = values[2]
_ = (&values).ptr + 1
_ = values.len
_ = values[0..2]
_ = "hello".ptr
_ = "hello".len
_ = point.x
_ = maybe?
_ = maybe orelse 0
_ = maybe orelse fallback()
_ = signed(1)
_ = unsigned(1)
_ = exact(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "target triple = \"arm64-apple-macosx13.0.0\""))
testing.expect(t, strings.contains(llvm_text, "declare signext i8 @signed(i8 signext)"))
testing.expect(t, strings.contains(llvm_text, "declare zeroext i8 @unsigned(i8 zeroext)"))
testing.expect(t, strings.contains(llvm_text, "declare i32 @exact(i32)"))
testing.expect(t, strings.contains(llvm_text, "@bro.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\""))
testing.expect(t, strings.contains(llvm_text, "getelementptr [3 x i8]"))
testing.expect(t, strings.contains(llvm_text, "attempted to unwrap none"))
testing.expect(t, strings.contains(llvm_text, "orelse_fallback"))
testing.expect(t, strings.contains(llvm_text, "orelse_some"))
}
@(test)
string_literals_preserve_static_length_and_sentinel_through_pointer_views :: proc(t: ^testing.T) {
text := `take_sentinel_pointer :: func(value [*;0]u8) void {}
take_mut_sentinel_pointer :: func(value [*;0]mut u8) void {}
take_pointer :: func(value *u8) void {}
take_sentinel_slice :: func(value [;0]u8) void {}
take_mut_sentinel_slice :: func(value [;0]mut u8) void {}
take_slice :: func(value []u8) void {}
take_c_string :: c_func(value *c_char) c_int
take_c_sentinel :: c_func(value [*;0]c_char) c_int
main :: func() void {
text :: "hello"
values [2;0]mut u8 = [1, 2]
pointer :: &values
_ = text.len
_ = text.ptr
_ = text[0]
_ = text[1..]
_ = pointer.len
_ = pointer.ptr
_ = pointer[0]
_ = pointer[1..]
offset [*;0]u8 :: text.ptr + 1
suffix [*;0]u8 :: text[1..].ptr
middle []u8 :: text[1..3]
_ = offset
_ = suffix
_ = middle
take_sentinel_pointer(text)
take_pointer(text)
take_sentinel_slice(text)
take_slice(text)
take_mut_sentinel_pointer(pointer)
take_mut_sentinel_slice(pointer)
take_sentinel_pointer(pointer)
take_sentinel_slice(pointer)
_ = take_c_string(text)
_ = take_c_sentinel(text)
_ = take_c_string(text.ptr)
_ = take_c_sentinel(text.ptr)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
string_type := types.INVALID
decays := 0
for expr in hir_module.exprs {
if expr.kind == .String {
string_type = expr.type
}
if expr.kind == .Decay_Array_Pointer {
decays += 1
}
}
pointer, array, string_ok := types.array_pointer(string_type, &hir_module.types)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, string_ok && !pointer.mutable && array.child == types.U8 &&
array.count == 5 && array.has_sentinel && array.sentinel == 0)
testing.expect(t, decays >= 6)
testing.expect(t, strings.contains(llvm_text, "@bro.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\""))
testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_string(ptr)"))
testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_sentinel(ptr)"))
}
@(test)
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)
c_variadic_calls_promote_extras_and_emit_variadic_llvm :: proc(t: ^testing.T) {
text := `variadic :: c_func(tag c_int, ...) c_int
zero :: c_func(...) void
main :: func() void {
narrow i8 :: -2
unsigned u16 :: 3
float_value f32 :: 4.0
c_float_value c_float :: 5.0
pointer *u8 :: "ok".ptr
nullable ?*u8 :: pointer
zero(pointer)
_ = variadic(7, narrow, unsigned, float_value, c_float_value, pointer, nullable)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
promotions := 0
for expr in hir_module.exprs {
if expr.kind == .C_Vararg_Promote {
promotions += 1
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, promotions, 4)
found_hir_variadic := false
for function in hir_module.functions {
found_hir_variadic = found_hir_variadic || function.variadic
}
found_ir_variadic := false
for function in ir_module.functions {
found_ir_variadic = found_ir_variadic || function.variadic
}
testing.expect(t, found_hir_variadic)
testing.expect(t, found_ir_variadic)
testing.expect(t, strings.contains(llvm_text, "declare i32 @variadic(i32, ...)"))
testing.expect(t, strings.contains(llvm_text, "declare void @zero(...)"))
testing.expect(t, strings.contains(llvm_text, "sext i8"))
testing.expect(t, strings.contains(llvm_text, "zext i16"))
testing.expect(t, strings.contains(llvm_text, "fpext float"))
testing.expect(t, strings.contains(llvm_text, "call void (...) @zero(ptr"))
testing.expect(t, strings.contains(llvm_text, "call i32 (i32, ...) @variadic(i32 7, i32"))
testing.expect(t, strings.contains(llvm_text, "double"))
testing.expect(t, strings.contains(llvm_text, "ptr"))
}
@(test)
c_variadic_restrictions_and_extra_argument_types_are_diagnosed :: proc(t: ^testing.T) {
text := `Record :: c_struct {
value c_int
}
foreign :: c_func(...) void
requires :: c_func(value c_int, ...) void
native :: func(...) void
bodyful :: c_func(...) void {}
main :: func() void {
values [1]u8 :: [1]
record Record :: Record { value = 1 }
foreign(values)
foreign(record)
requires()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
restricted := 0
found_extra := false
found_arity := false
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "must be a bodyless 'c_func' declaration") {
restricted += 1
}
found_extra = found_extra || strings.contains(diagnostic.message, "C variadic argument must be a concrete scalar or pointer")
found_arity = found_arity || strings.contains(diagnostic.message, "expects at least 1 arguments")
}
testing.expect_value(t, restricted, 2)
testing.expect(t, found_extra)
testing.expect(t, found_arity)
}
@(test)
variadicness_is_part_of_c_function_signature_compatibility :: proc(t: ^testing.T) {
fixed := ast.Function{result=types.C_INT}
variadic := ast.Function{result=types.C_INT, variadic=true}
testing.expect(t, !checker.function_signatures_equal(fixed, variadic))
testing.expect(t, !loader.function_signatures_equal(fixed, nil, types.C_INT, true))
}
@(test)
c_structs_are_by_value_and_may_be_opaque :: proc(t: ^testing.T) {
text := `Defined :: c_struct {
value c_int
}
Opaque :: c_struct
Empty :: c_struct {}
Bad :: c_struct {
values []i32
}
read :: c_func(value @Defined) c_int
pass :: c_func(value Defined) Defined
bad_opaque :: c_func(value Opaque) void
main :: func() void {
_ = pass(Defined { value = 1 })
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_opaque := false
found_bad_layout := false
found_empty := false
for diagnostic in diagnostics.items {
found_opaque = found_opaque || strings.contains(diagnostic.message, "cannot be passed by value")
found_bad_layout = found_bad_layout || strings.contains(diagnostic.message, "C-layout-compatible")
found_empty = found_empty || strings.contains(diagnostic.message, "at least one field")
}
testing.expect(t, found_opaque)
testing.expect(t, found_bad_layout)
testing.expect(t, found_empty)
}
@(test)
aarch64_c_record_abi_classifies_fixed_parameters_and_results :: proc(t: ^testing.T) {
text := `Small :: c_struct {
left c_int
right c_int
}
Medium :: c_struct {
first c_int
second c_int
third c_int
}
Hfa :: c_struct {
x c_float
y c_float
}
Large :: c_struct {
first c_long
second c_long
third c_long
}
small :: c_func(value Small) Small
medium :: c_func(value Medium) Medium
hfa :: c_func(value Hfa) Hfa
large :: c_func(value Large) Large
main :: func() void {
_ = small(Small { left = 1, right = 2 })
_ = medium(Medium { first = 1, second = 2, third = 3 })
_ = hfa(Hfa { x = 1.0, y = 2.0 })
_ = large(Large { first = 1, second = 2, third = 3 })
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "declare i64 @small(i64)"))
testing.expect(t, strings.contains(llvm_text, "declare [2 x i64] @medium([2 x i64])"))
testing.expect(t, strings.contains(llvm_text, "@hfa([2 x float])"))
testing.expect(t, strings.contains(llvm_text, "declare void @large(ptr sret("))
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64"))
}
@(test)
invalid_foreign_declarations_are_eagerly_diagnosed_and_calls_trap :: proc(t: ^testing.T) {
text := `bad :: c_func(value int) int
native :: func() i32
main :: func() void {
_ = bad(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_parameter := false
found_result := false
found_native := false
for diagnostic in diagnostics.items {
found_parameter = found_parameter || strings.contains(diagnostic.message, "requires concrete parameter types")
found_result = found_result || strings.contains(diagnostic.message, "requires a concrete or void result type")
found_native = found_native || strings.contains(diagnostic.message, "must use 'c_func'")
}
testing.expect(t, found_parameter)
testing.expect(t, found_result)
testing.expect(t, found_native)
testing.expect(t, !strings.contains(llvm_text, "@bad("))
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
}
@(test)
duplicate_foreign_symbols_across_packages_are_poisoned :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load("examples/packages/foreign_duplicate/app", &sources, &diagnostics, &symbols)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
duplicate_count := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "duplicate foreign symbol 'same'") {
duplicate_count += 1
}
}
testing.expect(t, loaded)
testing.expect_value(t, duplicate_count, 2)
testing.expect(t, !strings.contains(llvm_text, "@same("))
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
}
@(test)
bodyless_root_main_recovers_as_a_trap_definition :: proc(t: ^testing.T) {
text := "main :: c_func() i32\n"
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "main must have a body")
}
testing.expect(t, found)
testing.expect_value(t, len(ir_module.functions), 1)
testing.expect_value(t, ir_module.functions[0].implementation, ir.Implementation.Definition)
testing.expect(t, strings.contains(llvm_text, "define i32 @main()"))
testing.expect(t, !strings.contains(llvm_text, "declare i32 @main()"))
}
@(test)
literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) {
text := `return_i16 :: func() i16 {
return 1 + 2
}
take_i16 :: func(value i16) i16 {
return value
}
take_int :: func(value int) int {
return value
}
main :: func() void {
local i16 :: 1 + 2
_ = 100 + (20 + 8)
_ = return_i16()
_ = take_i16(1 + 2)
_ = take_int(127 + 1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__take_int__i16"))
for function in ir_module.functions {
for instruction in function.instructions {
testing.expect(t, instruction.op != ir.Opcode.Add_Checked)
}
}
for function in hir_module.functions {
for statement_id in function.body {
statement := hir_module.statements[statement_id]
if statement.expr < 0 {
continue
}
expr := hir_module.exprs[statement.expr]
if expr.kind == .Integer {
testing.expect(t, types.equal(expr.type, types.I16))
}
if expr.kind == .Call && symbol.resolve(&symbols, function.name) == "main" && len(expr.args) > 0 {
arg := hir_module.exprs[expr.args[0]]
testing.expect_value(t, arg.kind, hir.Expr_Kind.Integer)
testing.expect(t, types.equal(arg.type, types.I16))
}
}
}
}
@(test)
runtime_arithmetic_does_not_inherit_result_context :: proc(t: ^testing.T) {
text := `widen_after_add :: func(value i8) i16 {
return value + 1
}
main :: func() void {
_ = widen_after_add(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
found := false
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) != "widen_after_add" {
continue
}
found = true
statement := hir_module.statements[function.body[0]]
widen := hir_module.exprs[statement.expr]
add := hir_module.exprs[widen.left]
testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen)
testing.expect(t, types.equal(widen.type, types.I16))
testing.expect_value(t, add.kind, hir.Expr_Kind.Add)
testing.expect(t, types.equal(add.type, types.I8))
}
testing.expect(t, found)
}
@(test)
parser_recovers_after_invalid_tokens :: proc(t: ^testing.T) {
text := `broken @ declaration
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect(t, len(diagnostics.items) > 0)
testing.expect_value(t, len(module.functions), 1)
testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "main")
}
@(test)
return_sink_and_unconsumed_values_have_distinct_hir :: proc(t: ^testing.T) {
text := `give :: func() i8 {
return 1
}
done :: func() void {
return _
}
main :: func() void {
done()
_ = give()
give()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
done_id, main_id := -1, -1
for function, id in hir_module.functions {
if symbol.resolve(&symbols, function.name) == "done" {
done_id = id
} else if symbol.resolve(&symbols, function.name) == "main" {
main_id = id
}
}
testing.expect(t, done_id >= 0)
testing.expect(t, main_id >= 0)
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].kind, hir.Stmt_Kind.Return)
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].expr, hir.INVALID_EXPR)
main := hir_module.functions[main_id]
testing.expect_value(t, hir_module.statements[main.body[0]].kind, hir.Stmt_Kind.Expression)
testing.expect_value(t, hir_module.statements[main.body[1]].kind, hir.Stmt_Kind.Sink)
testing.expect_value(t, hir_module.statements[main.body[2]].kind, hir.Stmt_Kind.Trap)
}
@(test)
recursive_specialization_reaches_a_fixed_point :: proc(t: ^testing.T) {
text := `a :: func(value int) i32 {
return b(value)
}
b :: func(value int) i32 {
return a(value)
}
main :: func() void {
_ = a(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 3)
}
@(test)
stale_specializations_are_pruned_after_inference :: proc(t: ^testing.T) {
text := `derived :: identity(make())
wide :: delayed()
identity :: func(value int) int {
return value
}
make :: func() int {
return wide
return 1
}
delayed :: func() int {
return 128
}
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 4)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__identity__i16"))
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity__i8"))
}
@(test)
eager_global_calls_root_specializations :: proc(t: ^testing.T) {
text := `make :: func() i32 {
return 7
}
unused_native :: func() i32 {
return 9
}
unused_foreign :: c_func() i32
value i32 :: make()
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 2)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__make("))
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__unused_native("))
testing.expect(t, !strings.contains(llvm_text, "@unused_foreign("))
}
@(test)
malformed_generic_calls_do_not_retain_specializations :: proc(t: ^testing.T) {
text := `identity :: func(value int) int {
return value
}
bad :: identity(1, 2)
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_arity := false
for diagnostic in diagnostics.items {
found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 arguments, got 2")
}
testing.expect(t, found_arity)
testing.expect_value(t, len(hir_module.functions), 1)
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity"))
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
}
@(test)
long_generic_call_chain_reaches_a_fixed_point :: proc(t: ^testing.T) {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
for index in 0 ..< 70 {
fmt.sbprintf(&builder, "fn%d :: func(value int) int ", index)
strings.write_string(&builder, "{ return ")
if index == 69 {
strings.write_string(&builder, "value")
} else {
fmt.sbprintf(&builder, "fn%d(value)", index+1)
}
strings.write_string(&builder, " }\n")
}
strings.write_string(&builder, "main :: func() i32 { return fn0(1) }\n")
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 71)
}
@(test)
globals_and_generic_results_reach_a_shared_fixed_point :: proc(t: ^testing.T) {
text := `derived :: identity(base)
base :: make()
identity :: func(value int) int {
return value
}
make :: func() int {
return 1
}
main :: func() i32 {
return derived
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 3)
for global in hir_module.globals {
testing.expect(t, types.equal(global.type, types.I8))
}
}
@(test)
unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) {
text := `broken :: func(value, value i8, nope void) void {}
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_duplicate := false
found_void := false
for diagnostic in diagnostics.items {
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate parameter 'value'")
found_void = found_void || strings.contains(diagnostic.message, "void is only valid as a function result type")
}
testing.expect(t, found_duplicate)
testing.expect(t, found_void)
}
run_command_success :: proc(command: []string) -> bool {
state, stdout, stderr, err := os2.process_exec(
os2.Process_Desc{command=command},
context.allocator,
)
delete(stdout)
delete(stderr)
return err == nil && state.exit_code == 0
}
prepare_native_object :: proc(output: string) -> bool {
local_cache := fmt.aprintf("ZIG_LOCAL_CACHE_DIR=%s-zig-cache", output)
defer delete(local_cache)
global_cache := fmt.aprintf("ZIG_GLOBAL_CACHE_DIR=%s-zig-global-cache", output)
defer delete(global_cache)
return run_command_success([]string{
"/usr/bin/env",
local_cache,
global_cache,
"zig",
"cc",
"-c",
"examples/interop/manual/native.c",
"-o",
output,
})
}
prepare_native_archive :: proc(object, archive: string) -> bool {
return run_command_success([]string{
"/usr/bin/ar",
"-rcs",
archive,
object,
})
}
run_executable :: proc(path: string) -> os2.Process_State {
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{path}},
context.allocator,
)
delete(stdout)
delete(stderr)
return state
}
@(test)
valid_program_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-valid"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/prototype", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
c_printf_accepts_a_string_literal :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-printf"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/interop/printf", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
sentinel_pointer_views_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-sentinel-pointer"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/sentinel_pointer", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 303)
}
@(test)
control_flow_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-control-flow"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/control_flow", output)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
// if / else if / else, comparisons, logical and/or/not, bool locals, and
// block scoping together produce 42.
testing.expect_value(t, state.exit_code, 42)
// Short-circuit: `noisy()` is never reached, so its output must be absent,
// while the taken or-branch must print.
testing.expect(t, !strings.contains(string(stdout), "rhs-evaluated"))
testing.expect(t, strings.contains(string(stdout), "or-taken"))
}
@(test)
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)
unsupported_c_header_members_diagnose_only_when_referenced :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-header-unsupported"
defer _ = os.remove(output)
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
status := compiler_core.compile_package("examples/interop/header_unsupported", output, nil, target.DEFAULT, c_options)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
compatible_c_header_redeclarations_share_one_llvm_declaration :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-header-duplicate"
defer _ = os.remove(output)
arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}}
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
status := compiler_core.compile_package("examples/interop/header_duplicate", output, arguments, target.DEFAULT, c_options)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
interop_foundation_matches_zig_compiled_apple_silicon_c_fixture :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-interop-foundation"
defer _ = os.remove(output)
arguments := []linker.Argument{{kind=.Input, value="examples/interop/foundation/native.c"}}
status := compiler_core.compile_package("examples/interop/foundation", output, arguments)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 1)
}
@(test)
foreign_function_links_from_object :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-foreign-object"
object := "/tmp/brolang-test-foreign-object.o"
defer _ = os.remove(output)
defer _ = os.remove(object)
testing.expect(t, prepare_native_object(object))
arguments := []linker.Argument{{kind=.Input, value=object}}
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
foreign_function_links_from_direct_library :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-foreign-direct-library"
object := "/tmp/brolang-test-foreign-direct-library.o"
archive := "/tmp/brolang-test-foreign-direct-library.a"
defer _ = os.remove(output)
defer _ = os.remove(object)
defer _ = os.remove(archive)
testing.expect(t, prepare_native_object(object))
testing.expect(t, prepare_native_archive(object, archive))
arguments := []linker.Argument{{kind=.Input, value=archive}}
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
foreign_function_links_from_named_library :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-foreign-named-library"
object := "/tmp/brolang-test-foreign-named-library.o"
archive := "/tmp/libbrolang-test-foreign-named.a"
defer _ = os.remove(output)
defer _ = os.remove(object)
defer _ = os.remove(archive)
testing.expect(t, prepare_native_object(object))
testing.expect(t, prepare_native_archive(object, archive))
arguments := []linker.Argument{
{kind=.Library_Path, value="/tmp"},
{kind=.Library, value="brolang-test-foreign-named"},
}
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
one_line_main_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-one-line-main"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/one_line", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 7)
}
@(test)
folded_constant_addition_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-constant-fold"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/constant_fold", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unused_invalid_global_does_not_trap :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-invalid-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/invalid_unused_global", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
used_invalid_global_traps :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-invalid-used"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/invalid_used_global", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
main_int_is_constrained_to_i32 :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-main-int"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/main_int", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 3)
}
@(test)
main_i32_returns_directly :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-main-i32"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/main_i32", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 4)
}
@(test)
transitive_problematic_global_is_deferred :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-transitive-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/invalid_transitive_unused_global", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
checked_addition_traps_on_overflow :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-overflow"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/overflow", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
checked_runtime_negation_traps_for_every_signed_width :: proc(t: ^testing.T) {
Case :: struct {
type_name: string,
magnitude: string,
}
cases := [?]Case{
{type_name="i8", magnitude="128"},
{type_name="i16", magnitude="32768"},
{type_name="i32", magnitude="2147483648"},
{type_name="i64", magnitude="9223372036854775808"},
}
for test_case in cases {
directory := fmt.aprintf("/tmp/brolang-test-negate-overflow-%s", test_case.type_name)
main_path := fmt.aprintf("%s/main.bro", directory)
output := fmt.aprintf("/tmp/brolang-test-negate-overflow-output-%s", test_case.type_name)
builder := strings.builder_make()
fmt.sbprintf(
&builder,
"negate :: func(value %s) %s {{ return -value }}\nmain :: func() void {{ _ = negate(-%s) }}\n",
test_case.type_name,
test_case.type_name,
test_case.magnitude,
)
text := strings.clone(strings.to_string(builder))
strings.builder_destroy(&builder)
_ = os2.remove_all(directory)
_ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect(t, !state.success)
_ = os.remove(output)
_ = os2.remove_all(directory)
delete(text)
delete(output)
delete(main_path)
delete(directory)
}
}
@(test)
constant_that_does_not_fit_context_produces_trap_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-constant-context-error"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/constant_context_error", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
constant_beyond_i64_produces_trap_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-constant-i64-overflow"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/constant_i64_overflow", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
same_line_statements_are_diagnosed :: proc(t: ^testing.T) {
text := "main :: func() void { _ = 1 _ = 2 }\n"
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect(t, len(diagnostics.items) > 0)
}
@(test)
missing_main_produces_trap_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-missing-main"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/missing_main", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
backend_failure_preserves_existing_output :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-preserved-output"
defer _ = os.remove(output)
previous := "previous artifact"
testing.expect(t, os.write_entire_file(output, transmute([]byte)previous))
testing.expect(t, !backend.compile("/definitely/not/llvm.ll", output))
data, ok := os.read_entire_file(output)
defer delete(data)
testing.expect(t, ok)
testing.expect_value(t, string(data), previous)
}
@(test)
backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T) {
arguments := []linker.Argument{
{kind=.Input, value="native.c"},
{kind=.Library_Path, value="vendor/lib"},
{kind=.Library, value="thing"},
{kind=.Input, value="helper.o"},
}
c_options := cimport.Options{
include_paths=[]string{"vendor/include"},
defines=[]string{"FEATURE=1"},
}
command := backend.build_command("module.ll", "program", arguments, target.DEFAULT, c_options)
defer backend.destroy_command(command)
expected := []string{
"/usr/bin/env",
"zig",
"cc",
"-target",
"aarch64-macos",
"-Wno-override-module",
"-Wno-unused-command-line-argument",
"module.ll",
"-Ivendor/include",
"-DFEATURE=1",
"native.c",
"-Lvendor/lib",
"-lthing",
"helper.o",
"-o",
"program",
}
testing.expect_value(t, len(command), len(expected))
for value, index in expected {
testing.expect_value(t, command[index], value)
}
}
Fake_Cimport_State :: struct {
calls: int,
available: bool,
infrastructure: bool,
saw_options: bool,
}
Conflict_Cimport_State :: struct {
calls: int,
}
Symbol_Conflict_Cimport_State :: struct {
calls: int,
}
fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
state := (^Fake_Cimport_State)(user_data)
state.calls += 1
state.saw_options =
len(request.include_paths) == 2 &&
request.include_paths[0] == "first/include" &&
request.include_paths[1] == "second/include" &&
len(request.defines) == 2 &&
request.defines[0] == "FIRST=1" &&
request.defines[1] == "SECOND" &&
request.target == target.DEFAULT
result := cimport.init_result(allocator)
if !state.available {
result.infrastructure = state.infrastructure
result.error_message = fmt.aprintf("fake importer unavailable", allocator=allocator)
return result
}
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
append(&result.functions, cimport.Function{
name=fmt.aprintf("fake_value", allocator=allocator),
result=cimport.Type_Id(0),
variadic=true,
reason=fmt.aprintf("", allocator=allocator),
})
append(&result.variables, cimport.Variable{
name=fmt.aprintf("fake_global", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
append(&result.macros, cimport.Macro_Constant{
name=fmt.aprintf("FAKE_MAGIC", allocator=allocator),
type=cimport.Type_Id(0),
value={kind=.Integer, type=cimport.Type_Id(0), integer=7},
reason=fmt.aprintf("", allocator=allocator),
})
result.available = true
return result
}
conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
state := (^Conflict_Cimport_State)(user_data)
state.calls += 1
result := cimport.init_result(allocator)
kind := cimport.Type_Kind.C_Int
if strings.has_suffix(request.path, "second.h") {
kind = .C_Long
}
append(&result.types, cimport.Type{kind=kind, child=cimport.INVALID_TYPE})
append(&result.variables, cimport.Variable{
name=fmt.aprintf("conflict_global", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
result.available = true
return result
}
symbol_conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
state := (^Symbol_Conflict_Cimport_State)(user_data)
state.calls += 1
result := cimport.init_result(allocator)
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
if strings.has_suffix(request.path, "variable.h") {
append(&result.variables, cimport.Variable{
name=fmt.aprintf("conflict_symbol", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
} else {
append(&result.functions, cimport.Function{
name=fmt.aprintf("conflict_symbol", allocator=allocator),
result=cimport.Type_Id(0),
reason=fmt.aprintf("", allocator=allocator),
})
}
result.available = true
return result
}
main_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
result := cimport.init_result(allocator)
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
append(&result.variables, cimport.Variable{
name=fmt.aprintf("main", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
result.available = true
return result
}
write_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
result := cimport.init_result(allocator)
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
append(&result.variables, cimport.Variable{
name=fmt.aprintf("write", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
result.available = true
return result
}
count_substring_occurrences :: proc(text, needle: string) -> int {
if len(needle) == 0 {
return 0
}
count := 0
for index := 0; index + len(needle) <= len(text); index += 1 {
if text[index:index + len(needle)] == needle {
count += 1
}
}
return count
}
find_substring_offset :: proc(text, needle: string) -> int {
if len(needle) == 0 {
return 0
}
for index := 0; index + len(needle) <= len(text); index += 1 {
if text[index:index + len(needle)] == needle {
return index
}
}
return -1
}
find_cimport_variable :: proc(result: ^cimport.Result, name: string) -> (^cimport.Variable, bool) {
for &variable in result.variables {
if variable.name == name {
return &variable, true
}
}
return nil, false
}
count_cimport_variables :: proc(result: ^cimport.Result, name: string) -> int {
count := 0
for variable in result.variables {
if variable.name == name {
count += 1
}
}
return count
}
find_cimport_macro :: proc(result: ^cimport.Result, name: string) -> (^cimport.Macro_Constant, bool) {
for &macro in result.macros {
if macro.name == name {
return &macro, true
}
}
return nil, false
}
cimport_has_named_result :: proc(result: ^cimport.Result, name: string) -> bool {
if _, ok := find_cimport_macro(result, name); ok {
return true
}
for item in result.unsupported {
if item.name == name {
return true
}
}
return false
}
@(test)
cimport_backend_is_replaceable :: proc(t: ^testing.T) {
state := Fake_Cimport_State{available=true}
options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}}
result := cimport.import_header(options, "fake.h")
defer cimport.destroy_result(&result)
testing.expect(t, result.available)
testing.expect_value(t, state.calls, 1)
testing.expect_value(t, len(result.functions), 1)
testing.expect_value(t, len(result.variables), 1)
testing.expect_value(t, len(result.macros), 1)
testing.expect_value(t, result.functions[0].name, "fake_value")
testing.expect_value(t, result.variables[0].name, "fake_global")
testing.expect_value(t, result.macros[0].name, "FAKE_MAGIC")
testing.expect(t, result.functions[0].variadic)
}
@(test)
libclang_import_preserves_external_object_and_final_macro_semantics :: proc(t: ^testing.T) {
options := cimport.Options{
include_paths=[]string{"examples/interop/header/include"},
defines=[]string{"BROLANG_FEATURE"},
}
result := cimport.import_header(
options,
"examples/interop/header/include/native.h",
target.DEFAULT,
)
defer cimport.destroy_result(&result)
testing.expect(t, result.available)
testing.expect_value(t, result.error_message, "")
enum_alias_type := cimport.INVALID_TYPE
for alias in result.aliases {
if alias.name == "Imported_Enum" {
enum_alias_type = alias.type
break
}
}
testing.expect(t, enum_alias_type != cimport.INVALID_TYPE)
if enum_alias_type != cimport.INVALID_TYPE {
testing.expect_value(t, result.types[enum_alias_type].kind, cimport.Type_Kind.C_Int)
}
enum_negative, found_enum_negative := find_cimport_macro(&result, "IMPORTED_ENUM_NEGATIVE")
enum_same, found_enum_same := find_cimport_macro(&result, "IMPORTED_ENUM_SAME")
enum_value, found_enum_value := find_cimport_macro(&result, "IMPORTED_ENUM_VALUE")
enum_back, found_enum_back := find_cimport_macro(&result, "IMPORTED_ENUM_BACK")
enum_anon, found_enum_anon := find_cimport_macro(&result, "IMPORTED_ANON_ENUM")
testing.expect(t, found_enum_negative && found_enum_same && found_enum_value && found_enum_back && found_enum_anon)
if found_enum_negative {
testing.expect(t, enum_negative.value.negative)
testing.expect_value(t, enum_negative.value.integer, u64(2))
}
if found_enum_same {
testing.expect(t, enum_same.value.negative)
testing.expect_value(t, enum_same.value.integer, u64(2))
}
if found_enum_value {
testing.expect(t, !enum_value.value.negative)
testing.expect_value(t, enum_value.value.integer, u64(7))
}
if found_enum_back {
testing.expect_value(t, enum_back.value.integer, u64(3))
}
if found_enum_anon {
testing.expect_value(t, enum_anon.value.integer, u64(9))
}
tls, found_tls := find_cimport_variable(&result, "imported_tls_global")
testing.expect(t, found_tls)
if found_tls {
testing.expect(t, strings.contains(tls.reason, "thread-local C variables are not supported"))
}
const_array, found_const_array := find_cimport_variable(&result, "imported_const_array")
testing.expect(t, found_const_array)
if found_const_array {
testing.expect(t, !const_array.mutable)
testing.expect(t, const_array.type != cimport.INVALID_TYPE)
if const_array.type != cimport.INVALID_TYPE {
array_type := result.types[const_array.type]
testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array)
testing.expect(t, array_type.child != cimport.INVALID_TYPE)
if array_type.child != cimport.INVALID_TYPE {
testing.expect_value(t, result.types[array_type.child].kind, cimport.Type_Kind.C_Int)
}
}
}
typedef_const_array, found_typedef_const_array := find_cimport_variable(
&result, "imported_typedef_const_array",
)
testing.expect(t, found_typedef_const_array)
if found_typedef_const_array {
testing.expect(t, !typedef_const_array.mutable)
testing.expect_value(t, typedef_const_array.reason, "")
testing.expect(t, typedef_const_array.type != cimport.INVALID_TYPE)
if typedef_const_array.type != cimport.INVALID_TYPE {
array_type := result.types[typedef_const_array.type]
testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array)
testing.expect(t, !array_type.mutable)
testing.expect_value(t, array_type.count, u64(2))
}
}
redeclared_array, found_redeclared_array := find_cimport_variable(
&result, "imported_redeclared_array",
)
testing.expect(t, found_redeclared_array)
testing.expect_value(t, count_cimport_variables(&result, "imported_redeclared_array"), 1)
if found_redeclared_array {
testing.expect_value(t, redeclared_array.reason, "")
testing.expect(t, redeclared_array.type != cimport.INVALID_TYPE)
if redeclared_array.type != cimport.INVALID_TYPE {
array_type := result.types[redeclared_array.type]
testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array)
testing.expect_value(t, array_type.count, u64(4))
}
}
repeated, found_repeated := find_cimport_macro(&result, "IMPORTED_REPEAT")
testing.expect(t, found_repeated)
if found_repeated {
testing.expect_value(t, repeated.value.integer, u64(123))
}
testing.expect(t, !cimport_has_named_result(&result, "IMPORTED_GUARDED"))
testing.expect(t, !cimport_has_named_result(&result, "IMPORTED_ONCE"))
negative_decimal, found_negative_decimal := find_cimport_macro(&result, "IMPORTED_NEG_DECIMAL")
testing.expect(t, found_negative_decimal)
if found_negative_decimal {
testing.expect_value(t, result.types[negative_decimal.type].kind, cimport.Type_Kind.C_Long)
testing.expect_value(t, negative_decimal.value.integer, u64(2147483648))
testing.expect(t, negative_decimal.value.negative)
}
negative_hex, found_negative_hex := find_cimport_macro(&result, "IMPORTED_NEG_HEX")
testing.expect(t, found_negative_hex)
if found_negative_hex {
testing.expect_value(t, result.types[negative_hex.type].kind, cimport.Type_Kind.C_Uint)
testing.expect_value(t, negative_hex.value.integer, u64(0x80000000))
testing.expect(t, !negative_hex.value.negative)
}
negative_uint, found_negative_uint := find_cimport_macro(&result, "IMPORTED_NEG_UINT")
testing.expect(t, found_negative_uint)
if found_negative_uint {
testing.expect_value(t, result.types[negative_uint.type].kind, cimport.Type_Kind.C_Uint)
testing.expect_value(t, negative_uint.value.integer, u64(0xffffffff))
testing.expect(t, !negative_uint.value.negative)
}
conversions, found_conversions := find_cimport_macro(&result, "IMPORTED_CONVERSIONS")
testing.expect(t, found_conversions)
if found_conversions {
testing.expect_value(t, len(conversions.values), 5)
expected_kinds := [?]cimport.Type_Kind{
.C_Int,
.C_Double,
.C_Int,
.C_Float,
.C_Int,
}
for value, index in conversions.values {
testing.expect(t, value.type != cimport.INVALID_TYPE)
if value.type != cimport.INVALID_TYPE {
testing.expect_value(t, result.types[value.type].kind, expected_kinds[index])
}
}
}
}
@(test)
final_macros_override_same_named_c_value_declarations :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
c_options := cimport.Options{
include_paths=[]string{"examples/interop/header/include"},
defines=[]string{"BROLANG_FEATURE"},
}
module, loaded := loader.load(
"examples/interop/header/app",
&sources,
&diagnostics,
&symbols,
c_options=c_options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_OBJECT = external"))
testing.expect(t, !strings.contains(llvm_text, "declare i32 @IMPORTED_SHADOW_FUNCTION("))
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_UNSUPPORTED = external"))
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_EMPTY_SHADOW = external"))
}
@(test)
static_inline_c_functions_route_through_generated_trampolines :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
c_options := cimport.Options{
include_paths=[]string{"examples/interop/header/include"},
defines=[]string{"BROLANG_FEATURE"},
}
module, loaded := loader.load(
"examples/interop/header/app",
&sources,
&diagnostics,
&symbols,
c_options=c_options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
// Symbols are namespaced by a header-path hash, so match by suffix.
scalar_symbol := ""
record_symbol := ""
for trampoline in module.c_trampolines {
testing.expect(t, strings.has_prefix(trampoline.symbol, "__brolang_inline_"))
if strings.has_suffix(trampoline.symbol, "_imported_inline") {
scalar_symbol = trampoline.symbol
}
if strings.has_suffix(trampoline.symbol, "_imported_inline_record") {
record_symbol = trampoline.symbol
}
// The variadic static inline is unsupported and must not be wrapped.
testing.expect(t, !strings.has_suffix(trampoline.symbol, "_imported_inline_variadic"))
}
testing.expect(t, scalar_symbol != "")
testing.expect(t, record_symbol != "")
// A static inline whose signature translates but is rejected by the loader's
// by-value layout checks keeps its cimport-assigned link_name yet must not
// emit a wrapper — it is uncallable, so the wrapper would be dead code.
bad_layout_link := ""
for function in module.functions {
if symbol.resolve(&symbols, function.name) == "imported_inline_bad_layout" {
testing.expect(t, len(function.unsupported_reason) > 0)
bad_layout_link = function.link_name
}
}
testing.expect(t, bad_layout_link != "") // cimport did generate a wrapper symbol
for trampoline in module.c_trampolines {
testing.expect(t, trampoline.symbol != bad_layout_link)
}
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, fmt.tprintf("@%s(", scalar_symbol)))
testing.expect(t, strings.contains(llvm_text, fmt.tprintf("@%s(", record_symbol)))
// The internal-linkage C symbol itself is never declared or called directly.
testing.expect(t, !strings.contains(llvm_text, "@imported_inline("))
}
@(test)
loader_injects_and_caches_cimport_backend_per_compilation :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
state := Fake_Cimport_State{available=true}
options := cimport.Options{
include_paths=[]string{"first/include", "second/include"},
defines=[]string{"FIRST=1", "SECOND"},
backend={import_header=fake_cimport_backend, user_data=&state},
}
module, loaded := loader.load(
"examples/interop/header_cache",
&sources,
&diagnostics,
&symbols,
c_options=options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, state.calls, 1)
testing.expect(t, state.saw_options)
testing.expect_value(t, len(module.imports), 2)
testing.expect_value(t, module.imports[0].target, module.imports[1].target)
testing.expect_value(t, len(module.functions), 2)
found_variadic := false
for function in module.functions {
found_variadic = found_variadic || function.variadic
}
testing.expect(t, found_variadic)
found_external := false
found_macro := false
for global in module.globals {
name := symbol.resolve(&symbols, global.name)
found_external = found_external || (name == "fake_global" && global.external && global.writable)
found_macro = found_macro || name == "FAKE_MAGIC"
}
testing.expect(t, found_external)
testing.expect(t, found_macro)
}
@(test)
conflicting_external_c_globals_are_diagnosed_and_deduped :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
state := Conflict_Cimport_State{}
options := cimport.Options{backend={import_header=conflict_cimport_backend, user_data=&state}}
module, loaded := loader.load(
"examples/interop/header_conflict",
&sources,
&diagnostics,
&symbols,
c_options=options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
testing.expect_value(t, state.calls, 2)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_conflict := false
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "conflicting external C variable declarations for 'conflict_global'") {
found_conflict = true
}
}
testing.expect(t, found_conflict)
testing.expect_value(t, count_substring_occurrences(llvm_text, "@conflict_global = external global"), 1)
testing.expect(t, strings.contains(llvm_text, "@conflict_global = external global i32"))
testing.expect(t, !strings.contains(llvm_text, "@conflict_global = external global i64"))
}
@(test)
external_c_global_and_function_link_name_conflict_is_diagnosed :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
state := Symbol_Conflict_Cimport_State{}
options := cimport.Options{backend={import_header=symbol_conflict_cimport_backend, user_data=&state}}
module, loaded := loader.load(
"examples/interop/header_symbol_conflict",
&sources,
&diagnostics,
&symbols,
c_options=options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
testing.expect_value(t, state.calls, 2)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_conflict := false
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "external C variable 'conflict_symbol' conflicts with a C function declaration") {
found_conflict = true
}
}
testing.expect(t, found_conflict)
testing.expect(t, !strings.contains(llvm_text, "@conflict_symbol = external global"))
testing.expect(t, strings.contains(llvm_text, "declare i32 @conflict_symbol()"))
testing.expect(t, !strings.contains(llvm_text, "load i32, ptr @conflict_symbol"))
}
@(test)
external_c_global_named_main_is_omitted_for_root_entry_point :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
options := cimport.Options{backend={import_header=main_conflict_cimport_backend}}
module, loaded := loader.load(
"examples/interop/header_main_conflict",
&sources,
&diagnostics,
&symbols,
c_options=options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_conflict := false
for diagnostic in diagnostics.items {
found_conflict =
found_conflict ||
strings.contains(
diagnostic.message,
"external C variable 'main' conflicts with the program entry point",
)
}
testing.expect(t, found_conflict)
testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main("), 1)
testing.expect(t, !strings.contains(llvm_text, "@main = external"))
}
@(test)
external_c_global_named_main_is_omitted_for_synthesized_entry_point :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
options := cimport.Options{backend={import_header=main_conflict_cimport_backend}}
module, loaded := loader.load(
"examples/interop/header_main_conflict_missing",
&sources,
&diagnostics,
&symbols,
c_options=options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_conflict := false
found_missing_main := false
for diagnostic in diagnostics.items {
found_conflict =
found_conflict ||
strings.contains(
diagnostic.message,
"external C variable 'main' conflicts with the program entry point",
)
found_missing_main =
found_missing_main ||
strings.contains(diagnostic.message, "missing or unusable main function")
}
testing.expect(t, found_conflict)
testing.expect(t, found_missing_main)
testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main("), 1)
testing.expect(t, !strings.contains(llvm_text, "@main = external"))
}
@(test)
external_c_global_named_write_is_omitted_for_compiler_runtime :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
options := cimport.Options{backend={import_header=write_conflict_cimport_backend}}
module, loaded := loader.load(
"examples/interop/header_write_conflict",
&sources,
&diagnostics,
&symbols,
c_options=options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_conflict := false
for diagnostic in diagnostics.items {
found_conflict =
found_conflict ||
strings.contains(
diagnostic.message,
"external C variable 'write' conflicts with the compiler runtime",
)
}
testing.expect(t, found_conflict)
testing.expect_value(t, count_substring_occurrences(llvm_text, "declare i64 @write("), 1)
testing.expect(t, !strings.contains(llvm_text, "@write = external"))
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
}
@(test)
tls_reference_and_const_external_array_assignment_are_diagnosed :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
module, loaded := loader.load(
"examples/interop/header_unsupported",
&sources,
&diagnostics,
&symbols,
c_options=c_options,
)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_tls := false
found_excess_aggregate := false
found_signed_narrow := false
found_shadowed_unsupported := false
found_float_overflow := false
found_empty_shadow := false
found_inline_variadic := false
not_writable_count := 0
for diagnostic in diagnostics.items {
found_tls =
found_tls ||
strings.contains(
diagnostic.message,
"C declaration 'imported_tls_global' is unavailable: thread-local C variables are not supported",
)
found_excess_aggregate =
found_excess_aggregate ||
strings.contains(
diagnostic.message,
"C declaration 'IMPORTED_TOO_MANY_COLOR' is unavailable: C macro aggregate initializer is not representable",
)
found_signed_narrow =
found_signed_narrow ||
strings.contains(
diagnostic.message,
"C declaration 'IMPORTED_SIGNED_NARROW_BAD' is unavailable: C macro aggregate initializer is not representable",
)
found_shadowed_unsupported =
found_shadowed_unsupported ||
strings.contains(
diagnostic.message,
"C declaration 'IMPORTED_SHADOW_UNSUPPORTED' is unavailable: C macro is not a supported constant",
)
found_float_overflow =
found_float_overflow ||
strings.contains(
diagnostic.message,
"C declaration 'IMPORTED_FLOAT_OVERFLOW' is unavailable: C macro is not a supported constant",
)
found_empty_shadow =
found_empty_shadow ||
strings.contains(
diagnostic.message,
"C declaration 'IMPORTED_EMPTY_SHADOW' is unavailable: C macro has no replacement value",
)
found_inline_variadic =
found_inline_variadic ||
strings.contains(
diagnostic.message,
"C declaration 'imported_inline_variadic' is unavailable: variadic static inline C functions are not supported",
)
if strings.contains(diagnostic.message, "assignment target is not writable") {
not_writable_count += 1
}
}
testing.expect(t, found_tls)
testing.expect(t, found_excess_aggregate)
testing.expect(t, found_signed_narrow)
testing.expect(t, found_shadowed_unsupported)
testing.expect(t, found_float_overflow)
testing.expect(t, found_empty_shadow)
testing.expect(t, found_inline_variadic)
testing.expect(t, not_writable_count >= 5)
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_EMPTY_SHADOW = external"))
testing.expect(
t,
strings.contains(
llvm_text,
"@imported_typedef_const_array = external constant [2 x i32]",
),
)
testing.expect_value(
t,
count_substring_occurrences(
llvm_text,
"@imported_redeclared_array = external global [4 x i32]",
),
1,
)
testing.expect(
t,
!strings.contains(
llvm_text,
"ptr @imported_const_array_record, i64 0",
),
)
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_UNSUPPORTED = external"))
}
@(test)
cimport_infrastructure_failure_makes_compilation_unavailable :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
state := Fake_Cimport_State{infrastructure=true}
options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}}
module, loaded := loader.load(
"examples/interop/header_cache",
&sources,
&diagnostics,
&symbols,
c_options=options,
)
defer ast.destroy_module(&module)
testing.expect(t, !loaded)
testing.expect_value(t, state.calls, 1)
testing.expect(t, len(diagnostics.items) > 0)
}
@(test)
source_store_owns_buffers_indexes_lines_and_deduplicates_diagnostics :: proc(t: ^testing.T) {
store := source.init_store()
defer source.destroy_store(&store)
bytes := make([]byte, len("one\ntwo\n"))
copy(bytes, "one\ntwo\n")
source_id := source.add_source_owned(&store, "owned.bro", bytes)
bytes[0] = 'O'
diagnostics := source.init_store_diagnostics(&store)
defer source.destroy_diagnostics(&diagnostics)
span := source.Span{file=source_id, start=4, end=7}
first := source.add(&diagnostics, span, "same")
second := source.addf(&diagnostics, span, "%s", "same")
other := source.add(&diagnostics, span, "other")
formatted := source.format(&diagnostics, other)
defer delete(formatted)
testing.expect_value(t, store.items[source_id].text, "One\ntwo\n")
testing.expect_value(t, len(store.items[source_id].line_starts), 3)
testing.expect_value(t, first, second)
testing.expect_value(t, len(diagnostics.items), 2)
testing.expect(t, strings.contains(formatted, "owned.bro:2:1:"))
}
@(test)
maximum_signed_i64_literal_parses_exactly :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="value :: 9223372036854775807\nmain :: func() void {}\n"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(9223372036854775807))
}
@(test)
negative_constants_fold_and_accept_signed_i64_minimum :: proc(t: ^testing.T) {
text := `minimum :: -9223372036854775808
grouped i64 :: -(9223372036854775808)
folded :: -(1 + 2)
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, types.equal(hir_module.globals[0].type, types.I64))
testing.expect(t, types.equal(hir_module.globals[1].type, types.I64))
testing.expect(t, types.equal(hir_module.globals[2].type, types.I8))
testing.expect_value(t, hir_module.globals[0].static_value, i64(-9223372036854775807-1))
testing.expect_value(t, hir_module.globals[1].static_value, i64(-9223372036854775807-1))
testing.expect_value(t, hir_module.globals[2].static_value, i64(-3))
}
@(test)
constant_division_by_zero_has_a_precise_diagnostic :: proc(t: ^testing.T) {
text := `value :: 5 / 0
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_division_by_zero := false
found_overflow := false
for diagnostic in diagnostics.items {
found_division_by_zero = found_division_by_zero ||
strings.contains(diagnostic.message, "division by zero in constant expression")
found_overflow = found_overflow ||
strings.contains(diagnostic.message, "integer constant expression exceeds signed i64 range")
}
testing.expect(t, found_division_by_zero)
testing.expect(t, !found_overflow)
}
@(test)
out_of_range_negative_constants_are_diagnosed :: proc(t: ^testing.T) {
text := `positive :: 9223372036854775808
below_minimum :: -9223372036854775809
double_minimum :: --9223372036854775808
maximum_u64 :: 18446744073709551615
beyond_u64 :: 18446744073709551616
main :: func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_signed_range := 0
found_u64_range := false
for diagnostic in diagnostics.items {
found_signed_range += 1 if strings.contains(diagnostic.message, "exceeds signed i64 range") else 0
found_u64_range = found_u64_range || strings.contains(diagnostic.message, "magnitude does not fit in u64")
}
testing.expect_value(t, found_signed_range, 4)
testing.expect(t, found_u64_range)
}
@(test)
runtime_negation_preserves_operand_type_before_result_widening :: proc(t: ^testing.T) {
text := `negate_i8 :: func(value i8) i8 {
return -value
}
widen_after_negate :: func(value i8) i16 {
return -value
}
main :: func() void {
_ = negate_i8(1)
_ = widen_after_negate(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i8"))
found_widen := false
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) != "widen_after_negate" {
continue
}
statement := hir_module.statements[function.body[0]]
widen := hir_module.exprs[statement.expr]
negate := hir_module.exprs[widen.left]
testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen)
testing.expect(t, types.equal(widen.type, types.I16))
testing.expect_value(t, negate.kind, hir.Expr_Kind.Negate)
testing.expect(t, types.equal(negate.type, types.I8))
found_widen = true
}
testing.expect(t, found_widen)
}
@(test)
malformed_hir_references_lower_to_valid_trapped_llvm :: proc(t: ^testing.T) {
hir_module := hir.init_module()
defer hir.destroy_module(&hir_module)
append(&hir_module.exprs, hir.Expr{
kind=.Local,
type=types.I8,
target=hir.local_ref(hir.INVALID_LOCAL),
left=hir.INVALID_EXPR,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append(&hir_module.statements, hir.Stmt{
kind=.Expression,
expr=hir.Expr_Id(0),
local=hir.INVALID_LOCAL,
diagnostic=source.INVALID_DIAGNOSTIC,
})
body := make([]hir.Stmt_Id, 1)
body[0] = hir.Stmt_Id(0)
append(&hir_module.functions, hir.Function{
name=symbol.INVALID,
link_name=strings.clone("main"),
calling_convention=.C,
implementation=.Definition,
linkage=.External,
is_main=true,
result=types.VOID,
body=body,
diagnostic=source.INVALID_DIAGNOSTIC,
})
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
source_file := source.Source{path="test.bro", text=""}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(text)
testing.expect(t, strings.contains(text, "call void @bro.trap"))
testing.expect(t, !strings.contains(text, "%v-1"))
}
@(test)
malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) {
module := ir.init_module()
defer ir.destroy_module(&module)
instructions := make([]ir.Instruction, 4)
instructions[0] = ir.Instruction{op=.Store, type=types.I8, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
instructions[1] = ir.Instruction{op=.Add_Checked, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
instructions[2] = ir.Instruction{op=.Neg_Checked, type=types.I16, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
instructions[3] = ir.Instruction{op=.Return, type=types.I32, a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
append(&module.functions, ir.Function{
link_name=strings.clone("main"),
calling_convention=.C,
implementation=.Definition,
linkage=.External,
is_main=true,
result=types.I32,
instructions=instructions,
})
source_file := source.Source{path="test.bro", text=""}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
text := llvm.emit(&module, &diagnostics, &symbols)
defer delete(text)
testing.expect(t, strings.contains(text, "call void @bro.trap"))
testing.expect(t, strings.contains(text, "%v0 = add i8 0, -86"))
testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766"))
testing.expect(t, strings.contains(text, "%v2 = add i16 0, -21846"))
testing.expect(t, strings.contains(text, "@bro.trap(ptr %message, i64 %length) noreturn"))
testing.expect(t, !strings.contains(text, "%v-1"))
llvm_path := "/tmp/brolang-test-malformed-recovery.ll"
output := "/tmp/brolang-test-malformed-recovery"
defer _ = os.remove(llvm_path)
defer _ = os.remove(output)
testing.expect(t, os.write_entire_file(llvm_path, transmute([]byte)text))
testing.expect(t, backend.compile(llvm_path, output))
}
@(test)
hundred_thousand_term_runtime_addition_uses_iterative_pipeline :: proc(t: ^testing.T) {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
strings.write_string(&builder, "sum :: func(value i32) i32 { return value")
for _ in 0..<100_000 {
strings.write_string(&builder, " + 1")
}
strings.write_string(&builder, " }\nmain :: func() void { _ = sum(0) }\n")
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
instruction_count := 0
for function in ir_module.functions {
instruction_count += len(function.instructions)
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, instruction_count > 100_000)
}
@(test)
deep_global_cycle_detection_uses_iterative_dfs :: proc(t: ^testing.T) {
count := 50_000
ast_module := ast.init_module()
defer ast.destroy_module(&ast_module)
source_file := source.Source{path="test.bro", text=""}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
name := symbol.intern(&symbols, "value")
state := checker.Checker{
ast_module=&ast_module,
diagnostics=&diagnostics,
symbols=&symbols,
module=hir.init_module(),
allocator=context.allocator,
}
defer hir.destroy_module(&state.module)
defer delete(state.cycle_stack)
for id in 0..<count {
append(&ast_module.globals, ast.Global{name=name})
dependencies: [dynamic]hir.Global_Id
dependencies.allocator = context.allocator
append(&dependencies, hir.global_id((id+1)%count))
append(&state.module.globals, hir.Global{name=name, dependencies=dependencies})
}
states := make([]u8, count)
defer delete(states)
checker.detect_global_cycles_visit(&state, hir.Global_Id(0), states)
testing.expect_value(t, len(diagnostics.items), 1)
for global in state.module.globals {
testing.expect(t, global.problematic)
}
}
@(test)
mutable_local_reassignment_uses_runtime_storage :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-mutable"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/mutable_local", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 3)
}
@(test)
implicit_narrowing_produces_trap_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-narrowing"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/narrowing_error", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
valid_runtime_global_initializes_before_main :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-runtime-global"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/runtime_global", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unused_global_cycle_is_deferred :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-cycle-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/cycle_unused", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
used_global_cycle_traps :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-cycle-used"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/cycle_used", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
malformed_typed_values_still_produce_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-malformed-typed"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/malformed_typed_recovery", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
unused_invalid_function_body_is_not_checked :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-invalid-unused-function"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/invalid_unused_function", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
used_invalid_function_is_diagnosed_and_traps :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-invalid-used-function"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/invalid_used_function", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
function_mediated_problematic_global_is_deferred :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-function-global-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/function_global_unused", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
function_mediated_problematic_global_traps_when_used :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-function-global-used"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/function_global_used", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
package_files_merge_and_qualified_imports_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-basic"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/basic/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 3)
}
@(test)
unused_import_is_diagnosed_but_remains_executable :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/unused/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unused_missing_package_does_not_trap :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-missing-unused"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/missing_unused/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
referenced_missing_package_traps_at_reference :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-missing-used"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/missing_used/app", output)
testing.expect_value(t, status, 1)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect(t, !state.success)
testing.expect(t, strings.contains(string(stderr), "/missing_used/app/main.bro:4:6:"))
}
@(test)
imports_are_file_local :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-file-local"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/file_local/app", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(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)
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)
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)
conditional_unwrap_allows_sink_captures :: proc(t: ^testing.T) {
text := `main :: func() void {
first ?i32 = 1
second ?i32 = 2
if first and second |_, value : value == 2| {
_ = value
}
if first |_| {}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
main := hir_module.functions[0]
first_if := hir_module.statements[main.body[2]]
second_if := hir_module.statements[main.body[3]]
testing.expect_value(t, first_if.unwraps[0].local, hir.INVALID_LOCAL)
testing.expect(t, first_if.unwraps[1].local != hir.INVALID_LOCAL)
testing.expect_value(t, second_if.unwraps[0].local, hir.INVALID_LOCAL)
}
@(test)
parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^testing.T) {
cases := [4]struct {
text: string,
needle: string,
}{
{`main :: func() void {
value ?i32 = 1
if value || {}
}
`, "expected an unwrap capture name"},
{`main :: func() void {
value ?i32 = 1
if value |capture,| {}
}
`, "expected an unwrap capture after ','"},
{`main :: func() void {
value ?i32 = 1
if value |capture :| {}
}
`, "expected a guard expression after ':'"},
{`main :: func() void {
value ?i32 = 1
if value |capture {}
}
`, "expected '|' to close unwrap captures"},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
module := parser.parse(&stream, &source_file, &diagnostics)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.needle)
}
testing.expect(t, found)
ast.destroy_module(&module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
if_unwrap_on_non_optional_is_diagnosed :: proc(t: ^testing.T) {
text := `main :: func() i32 {
x i32 = 5
if x |v| {
return v
}
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "unwrap requires an optional")
}
testing.expect(t, found)
}
@(test)
conditional_unwrap_diagnostics_cover_counts_guards_and_capture_scope :: proc(t: ^testing.T) {
text := `main :: func() void {
first ?i32 = 1
second ?i32 = 2
plain i32 = 3
if first and second |one| {}
if first |one, two| {}
if first and second |same, same| {}
if plain |value| {}
if first |value : value| {}
if first and earlier |earlier, later| {}
if first |value| {
value = 2
}
if first |value| {
value i32 = 2
_ = value
}
if first |value| {
_ = value
} else {
_ = value
}
_ = value
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
count_mismatches := 0
duplicate := false
non_optional := false
guard := false
outer_operand_scope := false
immutable := false
redeclaration := false
capture_scope := 0
for diagnostic in diagnostics.items {
count_mismatches += 1 if strings.contains(diagnostic.message, "unwrap has") else 0
duplicate = duplicate || strings.contains(diagnostic.message, "unwrap captures must have distinct names")
non_optional = non_optional || strings.contains(diagnostic.message, "unwrap requires an optional value")
guard = guard || strings.contains(diagnostic.message, "unwrap guard must be a bool")
outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unresolved global 'earlier'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'value'")
capture_scope += 1 if strings.contains(diagnostic.message, "unresolved global 'value'") else 0
}
testing.expect_value(t, count_mismatches, 2)
testing.expect(t, duplicate)
testing.expect(t, non_optional)
testing.expect(t, guard)
testing.expect(t, outer_operand_scope)
testing.expect(t, immutable)
testing.expect(t, redeclaration)
testing.expect_value(t, capture_scope, 2)
}
@(test)
if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) {
// The binding `v` is usable in the then-block but not in the else-block.
text := `main :: func() i32 {
a ?i32 = 1
if a |v| {
return v
} else {
return v
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "unresolved global 'v'")
}
testing.expect(t, found)
}
@(test)
if_unwrap_binding_is_immutable :: proc(t: ^testing.T) {
text := `main :: func() i32 {
a ?i32 = 1
if a |v| {
v = 2
return v
}
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "cannot assign immutable local 'v'")
}
testing.expect(t, found)
}
@(test)
while_loops_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-while-loop"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/while_loop", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
while_loop_diagnostics_cover_condition_update_and_scope :: proc(t: ^testing.T) {
text := `bad_condition :: func() void {
while 1 {}
}
bad_unresolved :: func() void {
while false : missing = 1 {}
}
bad_immutable :: func() void {
i :: 0
while false : i = i + 1 {}
}
bad_body_scope :: func() void {
running :: false
while running : i = 1 {
i u32 = 0
}
}
bad_declaration_update :: func() void {
while false : i u32 = 0 {}
}
bad_missing_update :: func() void {
while false : {}
}
main :: func() void {
bad_condition()
bad_unresolved()
bad_immutable()
bad_body_scope()
bad_declaration_update()
bad_missing_update()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
non_bool := false
unresolved_missing := false
unresolved_body_local := false
immutable := false
disallowed := false
missing := false
for diagnostic in diagnostics.items {
non_bool = non_bool || strings.contains(diagnostic.message, "'while' condition must be a bool")
unresolved_missing = unresolved_missing || strings.contains(diagnostic.message, "cannot assign unresolved local 'missing'")
unresolved_body_local = unresolved_body_local || strings.contains(diagnostic.message, "cannot assign unresolved local 'i'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'i'")
disallowed = disallowed || strings.contains(diagnostic.message, "while update must be an assignment, sink, or expression statement")
missing = missing || strings.contains(diagnostic.message, "expected a while update statement after ':'")
}
testing.expect(t, non_bool)
testing.expect(t, unresolved_missing)
testing.expect(t, unresolved_body_local)
testing.expect(t, immutable)
testing.expect(t, disallowed)
testing.expect(t, missing)
}
@(test)
while_true_and_potential_fallthrough_have_distinct_return_analysis :: proc(t: ^testing.T) {
text := `forever :: func() i32 {
while true {}
}
maybe :: func(run bool) i32 {
while run {
return 1
}
}
main :: func() void {
if false {
_ = forever()
}
_ = maybe(false)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
missing_return_count := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "does not return a value") {
missing_return_count += 1
}
}
testing.expect_value(t, missing_return_count, 1)
}
@(test)
while_loop_allocas_are_emitted_in_the_entry_block :: proc(t: ^testing.T) {
text := `main :: func() i32 {
i u32 = 0
while i < 2 and true : i = i + 1 {
value u32 = i
_ = value
}
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
testing.expect(t, first_loop_label >= 0)
alloca_count := 0
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction, instruction_index in function.instructions {
if instruction.op != .Alloca {
continue
}
alloca_count += 1
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
offset := find_substring_offset(llvm_text, needle)
testing.expect(t, offset >= 0 && offset < first_loop_label)
}
}
testing.expect(t, alloca_count >= 3)
}
@(test)
for_loop_tokens_and_parser_capture_range_shape :: proc(t: ^testing.T) {
text := `main :: func() void {
for 0..4 |value| {
_ = value
}
items [1]mut i32 = [1]
for (&items) |@item, index| {
_ = item
_ = index
}
for 0..=1 |inclusive| {
_ = inclusive
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
for_count := 0
range_count := 0
inclusive_count := 0
for tok in stream.items {
#partial switch tok.kind {
case .Keyword_For: for_count += 1
case .Range: range_count += 1
case .Range_Inclusive: inclusive_count += 1
case:
}
}
testing.expect_value(t, for_count, 3)
testing.expect_value(t, range_count, 1)
testing.expect_value(t, inclusive_count, 1)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
function := module.functions[0]
first := module.statements[function.body[0]]
second := module.statements[function.body[2]]
third := module.statements[function.body[3]]
testing.expect_value(t, first.kind, ast.Stmt_Kind.For)
testing.expect(t, !first.pointer_capture)
testing.expect_value(t, module.exprs[first.expr].kind, ast.Expr_Kind.Range)
testing.expect_value(t, module.exprs[first.expr].integer, u64(0))
testing.expect_value(t, second.kind, ast.Stmt_Kind.For)
testing.expect(t, second.pointer_capture)
testing.expect(t, symbol.is_valid(second.index_name))
testing.expect_value(t, third.kind, ast.Stmt_Kind.For)
testing.expect_value(t, module.exprs[third.expr].integer, u64(1))
}
@(test)
range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) {
text := `main :: func() void {
limit :: 3
for 0..limit + 1 |bad| {
_ = bad
}
for 0..(limit + 1) |good| {
_ = good
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found := 0
for diagnostic in diagnostics.items {
found += 1 if strings.contains(diagnostic.message, "range bounds with operators must be parenthesized") else 0
}
testing.expect_value(t, found, 1)
}
@(test)
parser_diagnoses_malformed_for_captures :: proc(t: ^testing.T) {
cases := [4]struct {
text: string,
needle: string,
}{
{`main :: func() void {
for [1] item {}
}
`, "expected '|' before for-loop captures"},
{`main :: func() void {
for [1] |@| {}
}
`, "expected a for-loop item capture"},
{`main :: func() void {
for [1] |item,| {}
}
`, "expected an index capture after ','"},
{`main :: func() void {
for [1] |item {}
}
`, "expected '|' to close for-loop captures"},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
module := parser.parse(&stream, &source_file, &diagnostics)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.needle)
}
testing.expect(t, found)
ast.destroy_module(&module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
for_loops_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-for-loop"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/for_loop", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
range_loop_edges_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-for-loop-edges"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/for_loop_edges", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
for_loop_diagnostics_cover_iterables_captures_and_scope :: proc(t: ^testing.T) {
text := `bad_iterable :: func() void {
for 1 |item| {
_ = item
}
}
bad_array_pointer_capture :: func() void {
for [1] |@item| {
_ = item
}
}
bad_range_pointer_capture :: func() void {
for 0..1 |@item| {
_ = item
}
}
bad_range_index_capture :: func() void {
for 0..1 |item, index| {
_ = item
_ = index
}
}
bad_duplicate_capture :: func() void {
for [1] |item, item| {
_ = item
}
}
bad_capture_redeclaration :: func() void {
for [1] |item| {
item i32 = 2
_ = item
}
}
bad_capture_assignment :: func() void {
for [1] |item| {
item = 2
}
}
bad_immutable_pointer_capture :: func() void {
items :: [1]
for (&items) |@item| {
item^ = 2
}
}
bad_scope :: func() void {
for [1] |item| {
_ = item
}
_ = item
}
bad_integer_bounds :: func() void {
start i32 = 0
end u32 = 1
for start..end |item| {
_ = item
}
}
bad_float_bounds :: func() void {
for 0.0..1.0 |item| {
_ = item
}
}
main :: func() void {
bad_iterable()
bad_array_pointer_capture()
bad_range_pointer_capture()
bad_range_index_capture()
bad_duplicate_capture()
bad_capture_redeclaration()
bad_capture_assignment()
bad_immutable_pointer_capture()
bad_scope()
bad_integer_bounds()
bad_float_bounds()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
unsupported := false
array_pointer := false
range_pointer := false
range_index := false
duplicate_capture := false
redeclaration := false
immutable := false
immutable_pointer := false
scope := false
integer_bounds := 0
for diagnostic in diagnostics.items {
unsupported = unsupported || strings.contains(diagnostic.message, "for-loop iterable must be a range")
array_pointer = array_pointer || strings.contains(diagnostic.message, "pointer capture over an array requires")
range_pointer = range_pointer || strings.contains(diagnostic.message, "range loops do not support pointer captures")
range_index = range_index || strings.contains(diagnostic.message, "range loops do not support index captures")
duplicate_capture = duplicate_capture || strings.contains(diagnostic.message, "for-loop captures must have distinct names")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'item'")
immutable_pointer = immutable_pointer || strings.contains(diagnostic.message, "assignment target is not writable")
scope = scope || strings.contains(diagnostic.message, "unresolved global 'item'")
integer_bounds += 1 if strings.contains(diagnostic.message, "range bounds must be compatible concrete integers") else 0
}
testing.expect(t, unsupported)
testing.expect(t, array_pointer)
testing.expect(t, range_pointer)
testing.expect(t, range_index)
testing.expect(t, duplicate_capture)
testing.expect(t, redeclaration)
testing.expect(t, immutable)
testing.expect(t, immutable_pointer)
testing.expect(t, scope)
testing.expect_value(t, integer_bounds, 2)
}
@(test)
for_pointer_capture_respects_pointer_and_array_mutability :: proc(t: ^testing.T) {
text := `readonly :: func() void {
values [1]mut i32 = [1]
items @[1]mut i32 = &values
items[0] = 7
for items |@item| {
item^ = 7
}
}
writable :: func() void {
values [1]mut i32 = [1]
items @mut [1]mut i32 = &values
items[0] = 7
for items |@item| {
item^ = 7
}
}
main :: func() void {
readonly()
writable()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
readonly_errors := 0
for diagnostic in diagnostics.items {
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
}
testing.expect_value(t, readonly_errors, 2)
}
@(test)
pointer_field_passthrough_respects_pointee_mutability :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
}
readonly :: func(point @Point) i32 {
return point.x
}
bad_write :: func(point @Point) void {
point.x = 7
}
writable :: func(point @mut Point) void {
point.x += 1
}
main :: func() i32 {
point Point = Point { x = 41 }
writable(&point)
bad_write(&point)
return readonly(&point)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
readonly_errors := 0
for diagnostic in diagnostics.items {
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
}
testing.expect_value(t, readonly_errors, 1)
}
@(test)
equal_range_returns_infer_a_usable_result_type :: proc(t: ^testing.T) {
text := `choose :: func(first bool) int {
if first {
return 0..1
}
return 2..3
}
main :: func() i32 {
total i32 = 0
for choose(false) |value| {
total = total + value
}
return total
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
found := false
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) == "choose" {
found = true
testing.expect(t, types.is_range(function.result, &hir_module.types))
testing.expect_value(t, types.child_type(function.result, &hir_module.types), types.I8)
}
}
testing.expect(t, found)
testing.expect(t, strings.contains(llvm_text, "extractvalue"))
}
@(test)
for_loop_lowering_evaluates_once_and_avoids_index_bounds_checks :: proc(t: ^testing.T) {
text := `make_range :: func() int {
return 0..2
}
make_array :: func() int {
return [1, 2]
}
main :: func() i32 {
total i32 = 0
for make_range() |value| {
total = total + value
}
for make_array() |value| {
total = total + value
}
return total
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
call_count := 0
extract_count := 0
select_count := 0
pointer_add_count := 0
index_address_count := 0
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
testing.expect(t, first_loop_label >= 0)
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction, instruction_index in function.instructions {
#partial switch instruction.op {
case .Call: call_count += 1
case .Extract: extract_count += 1
case .Select: select_count += 1
case .Pointer_Add: pointer_add_count += 1
case .Index_Address: index_address_count += 1
case .Alloca:
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
offset := find_substring_offset(llvm_text, needle)
testing.expect(t, offset >= 0 && offset < first_loop_label)
case:
}
}
}
testing.expect_value(t, call_count, 2)
testing.expect_value(t, extract_count, 3)
testing.expect_value(t, select_count, 2)
testing.expect(t, pointer_add_count >= 1)
testing.expect_value(t, index_address_count, 0)
testing.expect(t, !strings.contains(llvm_text, "index_ok"))
}
@(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)
compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) {
// A compound assignment to an indexed lvalue must compute the element address
// once and reuse it for the load and the store, rather than re-lowering the
// lvalue (which would re-evaluate any side-effecting index subexpression).
text := `bump :: func() usize {
return 1
}
main :: func() i32 {
values [3]mut i32 = [10, 20, 30]
values[bump()] += 5
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
testing.expect_value(t, len(diagnostics.items), 0)
call_count := 0
index_address_count := 0
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction in function.instructions {
#partial switch instruction.op {
case .Call: call_count += 1
case .Index_Address: index_address_count += 1
case:
}
}
}
// `bump()` is the lvalue's index. The fix shares one address between the load
// and the store, so the side-effecting index runs exactly once and a single
// Index_Address is emitted; the buggy double-lowering produced two of each.
testing.expect_value(t, call_count, 1)
testing.expect_value(t, index_address_count, 1)
}
@(test)
compound_assignment_evaluates_nested_locations_once :: proc(t: ^testing.T) {
text := `Box :: struct {
value i32
}
row :: func() usize {
return 0
}
column :: func() usize {
return 1
}
pointer_for :: func(value @mut i32) @mut i32 {
return value
}
main :: func() i32 {
matrix [2]mut [2]mut i32 = [[1, 2], [3, 4]]
(matrix[row()])[column()] += 1
boxes [2]mut Box = [Box { value = 5 }, Box { value = 6 }]
boxes[row()].value += 1
value i32 = 7
pointer_for(&value)^ += 1
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
testing.expect_value(t, len(diagnostics.items), 0)
call_count := 0
call_names: [4]string
index_address_count := 0
field_address_count := 0
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction in function.instructions {
#partial switch instruction.op {
case .Call:
function_id := ir.as_function(instruction.target)
if call_count < len(call_names) &&
function_id != ir.INVALID_FUNCTION &&
int(function_id) < len(hir_module.functions) {
call_names[call_count] = symbol.resolve(
&symbols,
hir_module.functions[function_id].name,
)
}
call_count += 1
case .Index_Address: index_address_count += 1
case .Field_Address: field_address_count += 1
case:
}
}
}
// row(), column(), the second row(), and pointer_for() each run once. The
// nested matrix target needs two index addresses; the indexed field needs
// one index address and one field address.
testing.expect_value(t, call_count, 4)
testing.expect_value(t, call_names, [4]string{"row", "column", "row", "pointer_for"})
testing.expect_value(t, index_address_count, 3)
testing.expect_value(t, field_address_count, 1)
}
@(test)
compound_assignment_supports_pointer_add_only :: proc(t: ^testing.T) {
valid_text := `main :: func() i32 {
values [3]mut i32 = [10, 20, 30]
pointer *mut i32 = (&values).ptr
pointer += 1
offset usize = 1
pointer += offset
return pointer^
}
`
source_file := source.Source{path="test.bro", text=valid_text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
pointer_add_count := 0
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction in function.instructions {
pointer_add_count += 1 if instruction.op == .Pointer_Add else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, pointer_add_count, 2)
invalid_text := `main :: func() void {
values [1]mut i32 = [10]
pointer *mut i32 = (&values).ptr
pointer -= 1
}
`
invalid_source := source.Source{path="invalid.bro", text=invalid_text}
invalid_diagnostics := source.init_diagnostics(&invalid_source)
defer source.destroy_diagnostics(&invalid_diagnostics)
invalid_symbols := symbol.init_table()
defer symbol.destroy_table(&invalid_symbols)
invalid_stream := lexer.lex(&invalid_source, &invalid_diagnostics, &invalid_symbols)
defer delete(invalid_stream.items)
invalid_ast := parser.parse(&invalid_stream, &invalid_source, &invalid_diagnostics)
defer ast.destroy_module(&invalid_ast)
invalid_hir := checker.check(&invalid_ast, &invalid_diagnostics, &invalid_symbols)
defer hir.destroy_module(&invalid_hir)
found := false
for diagnostic in invalid_diagnostics.items {
found = found || strings.contains(
diagnostic.message,
"many-item pointers only support '+=' compound assignment",
)
}
testing.expect(t, found)
}
@(test)
compound_assignment_preserves_checked_numeric_operations :: proc(t: ^testing.T) {
text := `main :: func() i32 {
signed i32 = 24
signed += 6
signed -= 2
signed *= 3
signed /= 4
unsigned u32 = 24
unsigned += 6
unsigned -= 2
unsigned *= 3
unsigned /= 4
float f64 = 24.0
float += 6.0
float -= 2.0
float *= 3.0
float /= 4.0
return signed
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
operation_counts: [hir.Assignment_Op]int
for statement_id in hir_module.functions[0].body {
statement := hir_module.statements[statement_id]
if statement.kind == .Assignment {
operation_counts[statement.assignment_op] += 1
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, operation_counts[.Add], 3)
testing.expect_value(t, operation_counts[.Sub], 3)
testing.expect_value(t, operation_counts[.Mul], 3)
testing.expect_value(t, operation_counts[.Div], 3)
add_count := 0
sub_count := 0
mul_count := 0
div_count := 0
for instruction in ir_module.functions[0].instructions {
#partial switch instruction.op {
case .Add_Checked: add_count += 1
case .Sub_Checked: sub_count += 1
case .Mul_Checked: mul_count += 1
case .Div_Checked: div_count += 1
case:
}
}
testing.expect_value(t, add_count, 3)
testing.expect_value(t, sub_count, 3)
testing.expect_value(t, mul_count, 3)
testing.expect_value(t, div_count, 3)
}
@(test)
compound_assignment_rejects_narrowing_and_mixed_numeric_families :: proc(t: ^testing.T) {
text := `main :: func() void {
narrow i8 = 1
wide i32 = 2
narrow += wide
signed i32 = 3
unsigned u32 = 4
signed += unsigned
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_narrowing := false
found_mixed_family := false
for diagnostic in diagnostics.items {
found_narrowing = found_narrowing ||
strings.contains(diagnostic.message, "cannot implicitly convert i32 to i8")
found_mixed_family = found_mixed_family ||
strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
}
testing.expect(t, found_narrowing)
testing.expect(t, found_mixed_family)
}
@(test)
compound_assignment_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-compound"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/compound_assignment", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 23)
}
@(test)
binary_arithmetic_rejects_non_numeric_operands :: proc(t: ^testing.T) {
text := `main :: func() i32 {
a i32 = 1
b u32 = 2
_ = a / b
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
}
testing.expect(t, found)
}
@(test)
compound_assignment_requires_writable_target :: proc(t: ^testing.T) {
text := `main :: func() i32 {
x :: 5
x += 1
return x
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "assignment target is not writable")
}
testing.expect(t, found)
}
@(test)
checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) {
text := `main :: func() i32 {
a i32 = 10
b i32 = 3
c i32 = a - b
return c / b
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i32"))
testing.expect(t, strings.contains(llvm_text, "sdiv i32"))
testing.expect(t, strings.contains(llvm_text, "divzero_trap"))
testing.expect(t, strings.contains(llvm_text, "divovf_trap"))
}
@(test)
distinct_types_preserve_nominal_identity_and_backing_representation :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
y i32
}
UserID :: distinct u32
OtherID :: distinct u32
PointID :: distinct Point
Bytes :: distinct [2]u8
WrappedID :: distinct UserID
static_id UserID :: UserID(42)
take :: func(value UserID) UserID {
return value
}
main :: func() i32 {
id UserID :: UserID(7)
copy UserID = take(id)
maybe ?UserID = copy
pointer @UserID = &copy
point PointID :: PointID(Point { x = 1, y = 2 })
bytes Bytes :: Bytes([3, 4])
wrapped WrappedID :: WrappedID(id)
_ = maybe
_ = pointer
_ = point
_ = bytes
_ = wrapped
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
user_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "UserID")))
other_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "OtherID")))
point_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "PointID")))
point := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Point")))
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, user_id != other_id)
testing.expect(t, user_id != types.U32)
testing.expect(t, types.is_distinct(user_id, &ast_module.type_store))
testing.expect_value(t, types.runtime_representation(user_id, &ast_module.type_store), types.U32)
testing.expect_value(t, types.runtime_representation(point_id, &ast_module.type_store), point)
testing.expect_value(t, types.size(user_id, &ast_module.type_store), types.size(types.U32, &ast_module.type_store))
testing.expect(t, hir_module.globals[0].is_static)
testing.expect_value(t, hir_module.globals[0].static_value, i64(42))
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i32 42"))
testing.expect(t, strings.contains(llvm_text, "select i1 true, i32"))
retype_count := 0
for function in ir_module.functions {
for instruction in function.instructions {
retype_count += 1 if instruction.op == .Retype else 0
}
}
testing.expect_value(t, retype_count, 4)
}
@(test)
distinct_types_reject_implicit_conversions_operators_and_invalid_backings :: proc(t: ^testing.T) {
text := `Opaque :: c_struct
UserID :: distinct u32
OtherID :: distinct u32
BadInt :: distinct int
BadVoid :: distinct void
BadFunction :: distinct c_func() void
BadOpaque :: distinct Opaque
foreign :: c_func(value UserID) void
foreign_pointer :: c_func(value @UserID) void
main :: func() void {
raw u32 = 1
id UserID = raw
backing u32 = UserID(2)
other OtherID = UserID(3)
narrow u8 = 4
_ = UserID(narrow)
_ = UserID()
_ = UserID(1, 2)
left UserID :: UserID(5)
right UserID :: UserID(6)
_ = left + right
_ = left == right
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
invalid_backing_count := 0
implicit_conversion_count := 0
found_exact := false
found_arity := false
found_arithmetic := false
found_comparison := false
foreign_signature_count := 0
for diagnostic in diagnostics.items {
invalid_backing_count += 1 if strings.contains(diagnostic.message, "requires a concrete runtime backing type") else 0
implicit_conversion_count += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0
found_exact = found_exact || strings.contains(diagnostic.message, "requires an exact u32 value, got u8")
found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 argument")
found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
found_comparison = found_comparison || strings.contains(diagnostic.message, "comparison requires compatible numeric operands")
foreign_signature_count += 1 if strings.contains(diagnostic.message, "requires concrete parameter types") else 0
}
testing.expect_value(t, invalid_backing_count, 4)
testing.expect(t, implicit_conversion_count >= 3)
testing.expect(t, found_exact)
testing.expect(t, found_arity)
testing.expect(t, found_arithmetic)
testing.expect(t, found_comparison)
testing.expect_value(t, foreign_signature_count, 2)
}
@(test)
distinct_type_construction_defers_to_callable_names :: proc(t: ^testing.T) {
text := `Value :: distinct u32
Value :: func(value i32) i32 {
return value
}
main :: func() i32 {
return Value(42)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_call := false
found_retype := false
for expr in hir_module.exprs {
found_call = found_call || expr.kind == .Call
found_retype = found_retype || expr.kind == .Retype
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_call)
testing.expect(t, !found_retype)
}
@(test)
distinct_types_compile_and_run_across_packages :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-distinct-types"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/distinct_types", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
native_enums_preserve_identity_members_and_integer_representation :: proc(t: ^testing.T) {
text := `Animal :: enum {
dog
cat
bird
}
Nat :: enum(u16) {
one = 1
two
five = 5
}
global Animal :: Animal.dog
take :: func(value Animal) Animal {
return value
}
identity :: c_func(value Nat) Nat {
return value
}
variadic :: c_func(marker c_int, ...) c_int
main :: func() i32 {
value Animal = .cat
values [2]Animal :: [.dog, Animal.bird]
number Nat = identity(.two)
_ = variadic(0, number)
if take(value) == Animal.cat and values[0] != values[1] and number == Nat.two {
return 0
}
return 1
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
animal := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Animal")))
nat := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Nat")))
animal_node, animal_ok := types.node(&ast_module.type_store, animal)
nat_node, nat_ok := types.node(&ast_module.type_store, nat)
animal_members := types.enum_members_for(&ast_module.type_store, animal)
nat_members := types.enum_members_for(&ast_module.type_store, nat)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, animal_ok && nat_ok)
testing.expect(t, types.is_enum(animal, &ast_module.type_store))
testing.expect_value(t, animal_node.child, types.U8)
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(0))
testing.expect_value(t, animal_members[2].value, i128(2))
testing.expect_value(t, nat_members[0].value, i128(1))
testing.expect_value(t, nat_members[1].value, i128(2))
testing.expect_value(t, nat_members[2].value, i128(5))
testing.expect_value(t, types.runtime_representation(animal, &ast_module.type_store), types.U8)
testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(1))
testing.expect(t, hir_module.globals[0].is_static)
testing.expect_value(t, hir_module.globals[0].static_value, i64(0))
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i8 0"))
found_promotion := false
for function in ir_module.functions {
for instruction in function.instructions {
found_promotion = found_promotion || instruction.op == .C_Vararg_Promote
}
}
testing.expect(t, found_promotion)
}
@(test)
unbacked_enum_selects_the_smallest_fitting_unsigned_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)
}