Files
brolang/compiler_tests.odin
T
2026-08-02 21:14:49 +02:00

15971 lines
537 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/translatec"
import "./compiler/types"
import "core:fmt"
import "core:mem"
import "core:os"
import "core:os/os2"
import "core:strings"
import "core:testing"
@(test)
symbol_table_deduplicates_and_owns_spellings :: proc(t: ^testing.T) {
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
buffer := [5]byte{'a', 'l', 'p', 'h', 'a'}
alpha := symbol.intern(&symbols, string(buffer[:]))
duplicate := symbol.intern(&symbols, "alpha")
beta := symbol.intern(&symbols, "beta")
buffer[0] = 'x'
testing.expect_value(t, alpha, duplicate)
testing.expect(t, alpha != beta)
testing.expect_value(t, symbol.resolve(&symbols, alpha), "alpha")
testing.expect_value(t, symbol.resolve(&symbols, beta), "beta")
testing.expect_value(t, symbol.intern(&symbols, ""), symbol.INVALID)
testing.expect_value(t, symbol.resolve(&symbols, symbol.INVALID), "")
testing.expect(t, !symbol.is_valid(symbol.INVALID))
testing.expect(t, symbol.is_valid(alpha))
}
@(test)
compact_tokens_intern_only_identifiers_and_preserve_parser_text :: proc(t: ^testing.T) {
text := `other :: import "../math"
value :: 42
main func() void { _ = value }
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
value_symbol := symbol.intern(&symbols, "value")
sink_symbol := symbol.intern(&symbols, "_")
value_count := 0
for tok in stream.items {
#partial switch tok.kind {
case .Identifier:
if tok.symbol == value_symbol {
value_count += 1
}
case .Underscore:
testing.expect_value(t, tok.symbol, sink_symbol)
case:
testing.expect_value(t, tok.symbol, symbol.INVALID)
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, value_count, 2)
testing.expect_value(t, module.imports[0].path, "../math")
testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(42))
testing.expect_value(t, module.statements[module.functions[0].body[0]].name, sink_symbol)
}
@(test)
parser_marks_only_bang_calls_as_intrinsic :: proc(t: ^testing.T) {
text := `main func() void {
_ = sizeof ! (i32)
_ = sizeof(i32)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
marked := module.exprs[module.statements[module.functions[0].body[0]].expr]
ordinary := module.exprs[module.statements[module.functions[0].body[1]].expr]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, marked.kind, ast.Expr_Kind.Call)
testing.expect_value(t, ordinary.kind, ast.Expr_Kind.Call)
testing.expect(t, marked.intrinsic)
testing.expect(t, !ordinary.intrinsic)
}
@(test)
compact_ids_reserve_invalid_values_and_preserve_layout :: proc(t: ^testing.T) {
testing.expect_value(t, size_of(source.Span), 12)
testing.expect_value(t, size_of(token.Token), 24)
testing.expect(t, size_of(ast.Expr) <= 88)
testing.expect(t, size_of(hir.Expr) <= 88)
testing.expect(t, size_of(ir.Instruction) <= 88)
testing.expect_value(t, size_of(types.Type), 4)
source_index, source_ok := source.source_index(source.Source_Id(0), 1)
testing.expect_value(t, source_index, 0)
testing.expect(t, source_ok)
_, source_invalid := source.source_index(source.INVALID_SOURCE, 1)
testing.expect(t, !source_invalid)
_, source_out_of_bounds := source.source_index(source.Source_Id(1), 1)
testing.expect(t, !source_out_of_bounds)
diagnostic_index, diagnostic_ok := source.diagnostic_index(source.Diagnostic_Id(0), 1)
testing.expect_value(t, diagnostic_index, 0)
testing.expect(t, diagnostic_ok)
_, diagnostic_invalid := source.diagnostic_index(source.INVALID_DIAGNOSTIC, 1)
testing.expect(t, !diagnostic_invalid)
expr_index, expr_ok := ast.index(ast.Expr_Id(0), ast.INVALID_EXPR, 1)
testing.expect_value(t, expr_index, 0)
testing.expect(t, expr_ok)
_, expr_invalid := ast.index(ast.INVALID_EXPR, ast.INVALID_EXPR, 1)
testing.expect(t, !expr_invalid)
function_index, function_ok := hir.index(hir.Function_Id(0), hir.INVALID_FUNCTION, 1)
testing.expect_value(t, function_index, 0)
testing.expect(t, function_ok)
_, function_invalid := hir.index(hir.INVALID_FUNCTION, hir.INVALID_FUNCTION, 1)
testing.expect(t, !function_invalid)
instruction_index, instruction_ok := ir.index(ir.Instruction_Id(0), ir.INVALID_INSTRUCTION, 1)
testing.expect_value(t, instruction_index, 0)
testing.expect(t, instruction_ok)
_, instruction_invalid := ir.index(ir.INVALID_INSTRUCTION, ir.INVALID_INSTRUCTION, 1)
testing.expect(t, !instruction_invalid)
spec_index, spec_ok := checker.spec_index(checker.Spec_Id(0), 1)
testing.expect_value(t, spec_index, 0)
testing.expect(t, spec_ok)
_, spec_invalid := checker.spec_index(checker.INVALID_SPEC, 1)
testing.expect(t, !spec_invalid)
testing.expect(t, source.fits_source_length(u64(0xffff_ffff)))
testing.expect(t, !source.fits_source_length(u64(0x1_0000_0000)))
}
@(test)
lexer_preserves_newlines_and_skips_comments :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="# comment\nmain func() void {}\n"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, stream.items[0].kind, token.Kind.Newline)
testing.expect_value(t, stream.items[1].kind, token.Kind.Identifier)
}
@(test)
inline_is_keyword_and_expand_is_identifier :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="inline expand"}
diagnostics := source.init_diagnostics(&source_file)
defer 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.Keyword_Inline)
testing.expect_value(t, stream.items[1].kind, token.Kind.Identifier)
}
@(test)
lexer_recognizes_bitwise_operators_with_longest_match :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="~ & &= | |= xor xor= << <<= >> >>= <<| <<|= ^"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
expected := [?]token.Kind{
.Tilde, .Ampersand, .Ampersand_Equal, .Pipe, .Pipe_Equal, .Keyword_Xor, .Xor_Equal,
.Less_Less, .Less_Less_Equal, .Greater_Greater, .Greater_Greater_Equal,
.Less_Less_Pipe, .Less_Less_Pipe_Equal, .Caret, .Eof,
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(stream.items), len(expected))
for kind, index in expected {
testing.expect_value(t, stream.items[index].kind, kind)
}
}
@(test)
parser_applies_bitwise_precedence_and_preserves_capture_and_deref_pipes :: proc(t: ^testing.T) {
text := `main func() void {
_ = 1 + 2 << 1 & 7 xor 3 | 4 == 5 and true or false
value i32 = 1
pointer *i32 = &value
_ = pointer^ & 1
if (null | null) |captured| { _ = captured }
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer 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.statements[module.functions[0].body[0]].expr]
and_expr := module.exprs[root.left]
equality := module.exprs[and_expr.left]
bit_or := module.exprs[equality.left]
bit_xor := module.exprs[bit_or.left]
bit_and := module.exprs[bit_xor.left]
shift := module.exprs[bit_and.left]
addition := module.exprs[shift.left]
deref_and := module.exprs[module.statements[module.functions[0].body[3]].expr]
if_statement := module.statements[module.functions[0].body[4]]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, root.kind, ast.Expr_Kind.Or)
testing.expect_value(t, and_expr.kind, ast.Expr_Kind.And)
testing.expect_value(t, equality.kind, ast.Expr_Kind.Eq)
testing.expect_value(t, bit_or.kind, ast.Expr_Kind.Bit_Or)
testing.expect_value(t, bit_xor.kind, ast.Expr_Kind.Bit_Xor)
testing.expect_value(t, bit_and.kind, ast.Expr_Kind.Bit_And)
testing.expect_value(t, shift.kind, ast.Expr_Kind.Shift_Left)
testing.expect_value(t, addition.kind, ast.Expr_Kind.Add)
testing.expect_value(t, deref_and.kind, ast.Expr_Kind.Bit_And)
testing.expect_value(t, module.exprs[deref_and.left].kind, ast.Expr_Kind.Deref)
testing.expect_value(t, module.exprs[if_statement.expr].kind, ast.Expr_Kind.Bit_Or)
testing.expect_value(t, len(if_statement.captures), 1)
}
@(test)
bitwise_operations_lower_to_guarded_llvm_integer_instructions :: proc(t: ^testing.T) {
text := `ops func(a i32, b i32, count u8) i32 {
_ = ~a
_ = a & b
_ = a | b
_ = a xor b
_ = a << count
_ = a >> count
return a <<| count
}
uops func(a u32, count u8) u32 {
_ = a >> count
return a <<| count
}
main func() i32 {
_ = uops(4, 1)
return ops(1, 2, 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: [7]bool
for function in ir_module.functions {
for instruction in function.instructions {
#partial switch instruction.op {
case .Bit_Not: found[0] = true
case .Bit_And: found[1] = true
case .Bit_Or: found[2] = true
case .Bit_Xor: found[3] = true
case .Shift_Left: found[4] = true
case .Shift_Right: found[5] = true
case .Shift_Left_Saturating: found[6] = true
}
}
}
testing.expect_value(t, len(diagnostics.items), 0)
for present in found {
testing.expect(t, present)
}
testing.expect(t, strings.contains(llvm_text, " = and i32 "))
testing.expect(t, strings.contains(llvm_text, " = or i32 "))
testing.expect(t, strings.contains(llvm_text, " = xor i32 "))
testing.expect(t, strings.contains(llvm_text, " = shl i32 "))
testing.expect(t, strings.contains(llvm_text, " = ashr i32 "))
testing.expect(t, strings.contains(llvm_text, " = lshr i32 "))
testing.expect(t, strings.contains(llvm_text, "@llvm.sshl.sat.i32"))
testing.expect(t, strings.contains(llvm_text, "@llvm.ushl.sat.i32"))
testing.expect(t, strings.contains(llvm_text, "shift_in_range"))
testing.expect(t, strings.contains(llvm_text, "shift_trap"))
}
@(test)
typed_bitwise_constants_fold_in_runtime_expressions :: proc(t: ^testing.T) {
text := `D :: distinct u8
main func() void {
_ = ~D(1)
_ = ~u8(0)
_ = (u8(240) & u8(204)) xor u8(15)
_ = u8(129) << 1
_ = i8(-4) >> 1
_ = u8(1) <<| 8
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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)
d_type := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "D")))
for statement_id, index in hir_module.functions[0].body {
statement := hir_module.statements[statement_id]
testing.expect(t, statement.expr != hir.INVALID_EXPR)
if index == 0 {
testing.expect_value(t, hir_module.exprs[statement.expr].kind, hir.Expr_Kind.Bit_Not)
testing.expect_value(t, hir_module.exprs[statement.expr].type, d_type)
} else {
testing.expect_value(t, hir_module.exprs[statement.expr].kind, hir.Expr_Kind.Integer)
}
}
}
@(test)
bitwise_checker_rejects_invalid_operands_and_known_overshifts :: proc(t: ^testing.T) {
text := `D :: distinct u8
E :: enum { one }
main func() void {
p *u8 = null
d D = D(1)
e E = .one
signed_count i8 = 1
_ = true & false
_ = 1.0 | 2.0
_ = ~p
_ = d
_ = ~e
_ = u8(1) & i8(1)
_ = u8(1) << signed_count
_ = u8(1) << 8
_ = p >> u8(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_bitwise := false
found_shift := false
found_overshift := false
for diagnostic in diagnostics.items {
found_bitwise = found_bitwise || strings.contains(diagnostic.message, "bitwise") || strings.contains(diagnostic.message, "'~'")
found_shift = found_shift || strings.contains(diagnostic.message, "shift count") || strings.contains(diagnostic.message, "shifted value")
found_overshift = found_overshift || strings.contains(diagnostic.message, "exceeds u8 width")
}
testing.expect(t, found_bitwise)
testing.expect(t, found_shift)
testing.expect(t, found_overshift)
}
@(test)
runtime_ordinary_overshift_traps :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-bitwise-overshift"
main_path := "/tmp/brolang-test-bitwise-overshift/main.bro"
output := "/tmp/brolang-test-bitwise-overshift-output"
text := `shift func(value u8, count u8) u8 { return value << count }
main func() i32 {
_ = shift(1, 8)
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state, stdout, stderr, err := os2.process_exec(os2.Process_Desc{command=[]string{output}}, context.allocator)
defer delete(stdout)
defer delete(stderr)
testing.expect(t, err == nil)
testing.expect(t, state.exit_code != 0)
testing.expect(t, strings.contains(string(stderr), "shift count exceeds integer width"))
}
@(test)
runtime_distinct_ordinary_overshift_traps :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-distinct-bitwise-overshift"
main_path := "/tmp/brolang-test-distinct-bitwise-overshift/main.bro"
output := "/tmp/brolang-test-distinct-bitwise-overshift-output"
text := `D :: distinct u8
shift func(value D, count u8) D { return value << count }
main func() i32 {
_ = shift(D(1), 8)
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state, stdout, stderr, err := os2.process_exec(os2.Process_Desc{command=[]string{output}}, context.allocator)
defer delete(stdout)
defer delete(stderr)
testing.expect(t, err == nil)
testing.expect(t, state.exit_code != 0)
testing.expect(t, strings.contains(string(stderr), "shift count exceeds integer width"))
}
@(test)
parser_accepts_grouped_params_and_multiline_statements :: proc(t: ^testing.T) {
text := `sum func(a,
b int) int {
return (a
+ b)
}
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.functions), 2)
testing.expect_value(t, len(module.functions[0].params), 2)
}
@(test)
parser_accepts_comptime_value_params :: proc(t: ^testing.T) {
text := `make func($N usize, value i32) i32 {
return value
}
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found_dollar := false
for tok in stream.items {
found_dollar = found_dollar || tok.kind == token.Kind.Dollar
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_dollar)
testing.expect_value(t, len(module.functions[0].params), 2)
testing.expect(t, module.functions[0].params[0].comptime_value)
testing.expect(t, !module.functions[0].params[1].comptime_value)
}
@(test)
parser_accepts_grouped_comptime_params :: proc(t: ^testing.T) {
text := `grouped func($K, $V type, $A, B usize) 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)
params := module.functions[0].params
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(params), 4)
for param in params {
testing.expect(t, param.comptime_value)
}
testing.expect_value(t, params[0].type, params[1].type)
testing.expect_value(t, params[2].type, params[3].type)
}
@(test)
parser_accepts_comptime_type_params_and_builtin_type_args :: proc(t: ^testing.T) {
text := `id func($T type, value T) T {
return value
}
main func() void {
_ = id(i32, 42)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
call := module.exprs[module.statements[module.functions[1].body[0]].expr]
type_item, type_ok := types.node(&module.type_store, module.functions[0].params[0].type)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, module.functions[0].params[0].comptime_value)
testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].params[0].name), "T")
testing.expect(t, type_ok)
testing.expect_value(t, symbol.resolve(&symbols, symbol.Id(type_item.name)), "type")
testing.expect_value(t, call.kind, ast.Expr_Kind.Call)
testing.expect_value(t, module.exprs[call.args[0]].kind, ast.Expr_Kind.Type)
}
@(test)
parser_accepts_sentinel_many_item_pointer_types :: proc(t: ^testing.T) {
text := `zero func(value [*;0]u8) void {}
newline func(value [*;'\n']mut u8) void {}
nullable func(value ?[*;0]u8) void {}
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
zero, zero_ok := types.node(&module.type_store, module.functions[0].params[0].type)
newline, newline_ok := types.node(&module.type_store, module.functions[1].params[0].type)
nullable, nullable_ok := types.node(&module.type_store, module.functions[2].params[0].type)
nullable_child, nullable_child_ok := types.node(&module.type_store, nullable.child)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, zero_ok && zero.kind == .Pointer && zero.many && zero.has_sentinel && zero.sentinel == 0)
testing.expect(t, newline_ok && newline.kind == .Pointer && newline.many && newline.mutable &&
newline.has_sentinel && newline.sentinel == '\n')
testing.expect(t, nullable_ok && nullable.kind == .Optional)
testing.expect(t, nullable_child_ok && nullable_child.kind == .Pointer &&
nullable_child.many && nullable_child.has_sentinel)
}
@(test)
parser_accepts_c_function_pointer_types :: proc(t: ^testing.T) {
text := `take c_func(callback ?*c_func(value c_int) c_int) void
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
optional, optional_ok := types.node(&module.type_store, module.functions[0].params[0].type)
pointer, pointer_ok := types.node(&module.type_store, optional.child)
function, function_ok := types.node(&module.type_store, pointer.child)
params := types.params_for(&module.type_store, pointer.child)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, optional_ok && optional.kind == .Optional)
testing.expect(t, pointer_ok && pointer.kind == .Pointer && pointer.many && !pointer.mutable)
testing.expect(t, function_ok && function.kind == .Function && function.c_abi && !function.variadic)
testing.expect(t, function.child == types.C_INT)
testing.expect_value(t, len(params), 1)
testing.expect(t, params[0].type == types.C_INT)
}
@(test)
parser_accepts_native_function_pointer_types :: proc(t: ^testing.T) {
text := `Error :: enum {
bad
}
take func(callback ?@func(value i32) i32, fallible @func() i32 ! Error) void
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
optional, optional_ok := types.node(&module.type_store, module.functions[0].params[0].type)
pointer, pointer_ok := types.node(&module.type_store, optional.child)
function, function_ok := types.node(&module.type_store, pointer.child)
params := types.params_for(&module.type_store, pointer.child)
fallible_pointer, fallible_pointer_ok := types.node(&module.type_store, module.functions[0].params[1].type)
fallible_function, fallible_function_ok := types.node(&module.type_store, fallible_pointer.child)
fallible, fallible_ok := types.node(&module.type_store, fallible_function.child)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, optional_ok && optional.kind == .Optional)
testing.expect(t, pointer_ok && pointer.kind == .Pointer && !pointer.many && !pointer.mutable)
testing.expect(t, function_ok && function.kind == .Function && !function.c_abi && !function.variadic)
testing.expect(t, function.child == types.I32)
testing.expect_value(t, len(params), 1)
testing.expect(t, params[0].type == types.I32)
testing.expect(t, fallible_pointer_ok && fallible_pointer.kind == .Pointer && !fallible_pointer.many)
testing.expect(t, fallible_function_ok && fallible_function.kind == .Function && !fallible_function.c_abi)
testing.expect(t, fallible_ok && fallible.kind == .Fallible && fallible.child == types.I32)
}
@(test)
parser_accepts_c_function_pointer_alias_types :: proc(t: ^testing.T) {
text := `callback_alias :: alias ?*c_func(value i32) i32
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
name := symbol.intern(&symbols, "callback_alias")
alias := types.find_named(&module.type_store, 0, u32(name))
alias_node, alias_ok := types.node(&module.type_store, alias)
optional, optional_ok := types.node(&module.type_store, alias_node.child)
pointer, pointer_ok := types.node(&module.type_store, optional.child)
function, function_ok := types.node(&module.type_store, pointer.child)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, alias_ok && alias_node.kind == .Alias)
testing.expect(t, optional_ok && optional.kind == .Optional)
testing.expect(t, pointer_ok && pointer.kind == .Pointer)
testing.expect(t, function_ok && function.kind == .Function && function.c_abi)
}
@(test)
parser_rejects_old_function_declaration_binding_syntax :: proc(t: ^testing.T) {
text := `main :: func() void {}
foreign :: c_func() i32
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "function declarations do not use '::'") {
found += 1
}
}
testing.expect_value(t, found, 2)
testing.expect_value(t, len(module.functions), 2)
}
@(test)
parser_diagnoses_malformed_sentinel_pointer_types :: proc(t: ^testing.T) {
text := `bad func(value [*0]u8) void {}
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "expected ';' after '*' in sentinel pointer type")
}
testing.expect(t, found)
}
@(test)
parser_accepts_single_statement_one_line_functions :: proc(t: ^testing.T) {
text := `give func() i8 { return 7 }
main func() void { _ = give() }
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.functions), 2)
testing.expect_value(t, len(module.functions[0].body), 1)
testing.expect_value(t, len(module.functions[1].body), 1)
testing.expect_value(t, module.statements[module.functions[0].body[0]].kind, ast.Stmt_Kind.Return)
testing.expect_value(t, module.statements[module.functions[1].body[0]].kind, ast.Stmt_Kind.Assignment)
}
@(test)
parser_distinguishes_bodyless_declarations_and_definitions :: proc(t: ^testing.T) {
text := `foreign c_func(value i32) i32
defined c_func(value i32) i32
{
return value
}
native func() i32
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.functions), 4)
testing.expect(t, !module.functions[0].has_body)
testing.expect(t, module.functions[0].c_abi)
testing.expect(t, module.functions[1].has_body)
testing.expect(t, module.functions[1].c_abi)
testing.expect_value(t, len(module.functions[1].body), 1)
testing.expect(t, !module.functions[2].has_body)
testing.expect(t, !module.functions[2].c_abi)
testing.expect(t, module.functions[3].has_body)
}
@(test)
parser_accepts_terminal_c_variadic_markers_and_recovers_nonterminal_markers :: proc(t: ^testing.T) {
text := `fixed c_func(value c_int, ...) c_int
zero c_func(...) void
bad c_func(..., value c_int) c_int
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found_ellipsis := false
for tok in stream.items {
found_ellipsis = found_ellipsis || tok.kind == .Ellipsis
}
testing.expect(t, found_ellipsis)
testing.expect(t, module.functions[0].variadic)
testing.expect_value(t, len(module.functions[0].params), 1)
testing.expect(t, module.functions[1].variadic)
testing.expect_value(t, len(module.functions[1].params), 0)
testing.expect(t, module.functions[2].variadic)
testing.expect_value(t, len(module.functions[2].params), 1)
testing.expect_value(t, len(diagnostics.items), 1)
testing.expect(t, strings.contains(diagnostics.items[0].message, "final parameter"))
}
@(test)
parser_treats_c_as_contextual_only_before_func :: proc(t: ^testing.T) {
text := `c :: 5
x :: c
foreign c_func() i32
broken :: c 5
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
c_symbol := symbol.intern(&symbols, "c")
for tok in stream.items {
if int(tok.span.start) < len(text) && text[int(tok.span.start):int(tok.span.end)] == "c" {
testing.expect_value(t, tok.kind, token.Kind.Identifier)
testing.expect_value(t, tok.symbol, c_symbol)
}
}
testing.expect_value(t, len(module.globals), 3)
testing.expect_value(t, module.exprs[module.globals[1].expr].name, c_symbol)
testing.expect_value(t, module.exprs[module.globals[2].expr].name, c_symbol)
testing.expect(t, module.functions[0].c_abi)
testing.expect_value(t, len(diagnostics.items), 1)
testing.expect(t, strings.contains(diagnostics.items[0].message, "followed by a newline"))
}
@(test)
parser_accepts_undefined_expression :: proc(t: ^testing.T) {
text := `main func() void {
value i32 = undefined
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found_keyword := false
for tok in stream.items {
found_keyword = found_keyword || tok.kind == .Keyword_Undefined
}
statement := module.statements[module.functions[0].body[0]]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_keyword)
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Undefined)
}
@(test)
pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="value :: 1 + 2 + 3\nmain func() void {}\n"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
root := module.exprs[module.globals[0].expr]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, root.kind, ast.Expr_Kind.Add)
testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Add)
testing.expect_value(t, module.exprs[root.right].integer, u64(3))
}
@(test)
pratt_parser_handles_prefix_negation_precedence :: proc(t: ^testing.T) {
text := `identity func(value i8) i8 { return value }
loose :: -1 + 2
grouped :: -(1 + 2)
called :: -identity(1)
chained :: --1
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
loose := module.exprs[module.globals[0].expr]
grouped := module.exprs[module.globals[1].expr]
called := module.exprs[module.globals[2].expr]
chained := module.exprs[module.globals[3].expr]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, loose.kind, ast.Expr_Kind.Add)
testing.expect_value(t, module.exprs[loose.left].kind, ast.Expr_Kind.Negate)
testing.expect_value(t, grouped.kind, ast.Expr_Kind.Negate)
testing.expect_value(t, module.exprs[grouped.left].kind, ast.Expr_Kind.Add)
testing.expect_value(t, called.kind, ast.Expr_Kind.Negate)
testing.expect_value(t, module.exprs[called.left].kind, ast.Expr_Kind.Call)
testing.expect_value(t, chained.kind, ast.Expr_Kind.Negate)
testing.expect_value(t, module.exprs[chained.left].kind, ast.Expr_Kind.Negate)
}
@(test)
parser_accepts_comptime_prefix_and_block_expressions :: proc(t: ^testing.T) {
text := `sum func(a, b int) int { return a + b }
literal :: $32
call :: $sum(1, 2) + 3
block :: ${
yield 4
}
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
literal := module.exprs[module.globals[0].expr]
call_add := module.exprs[module.globals[1].expr]
call := module.exprs[call_add.left]
block := module.exprs[module.globals[2].expr]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, literal.kind, ast.Expr_Kind.Comptime)
testing.expect_value(t, module.exprs[literal.left].kind, ast.Expr_Kind.Integer)
testing.expect_value(t, call_add.kind, ast.Expr_Kind.Add)
testing.expect_value(t, call.kind, ast.Expr_Kind.Comptime)
testing.expect_value(t, module.exprs[call.left].kind, ast.Expr_Kind.Call)
testing.expect_value(t, block.kind, ast.Expr_Kind.Comptime)
testing.expect_value(t, len(block.body), 1)
testing.expect_value(t, module.statements[block.body[0]].kind, ast.Stmt_Kind.Yield)
}
nested_expression_source :: proc(call: bool, depth: int) -> string {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
if call {
strings.write_string(&builder, "identity func(value i32) i32 { return value }\nvalue :: ")
for _ in 0..<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",
"--root",
".",
"--target",
"aarch64-macos",
})
defer delete(options.link_arguments)
defer delete(options.c_options.include_paths)
defer delete(options.c_options.defines)
testing.expect(t, valid)
testing.expect_value(t, options.input_path, "app")
testing.expect_value(t, options.output_path, "app.out")
testing.expect_value(t, options.project_root, ".")
testing.expect_value(t, len(options.link_arguments), 4)
testing.expect_value(t, options.link_arguments[0].kind, linker.Kind.Input)
testing.expect_value(t, options.link_arguments[0].value, "native.c")
testing.expect_value(t, options.link_arguments[1].kind, linker.Kind.Library_Path)
testing.expect_value(t, options.link_arguments[2].kind, linker.Kind.Library)
testing.expect_value(t, options.link_arguments[3].value, "helper.o")
testing.expect_value(t, options.c_options.include_paths[0], "vendor/include")
testing.expect_value(t, options.c_options.defines[0], "FEATURE=1")
testing.expect_value(t, target.name(options.target), "aarch64-macos")
_, unknown_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--unknown", "value"})
_, incomplete_valid := parse_cli_args([]string{"brolang", "app", "-o"})
_, duplicate_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "one", "-o", "two"})
_, duplicate_root_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--root", ".", "--root", "other"})
_, duplicate_empty_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "", "-o", "two"})
_, invalid_target := parse_cli_args([]string{"brolang", "app", "-o", "out", "--target", "x86_64-linux"})
_, legacy_link := parse_cli_args([]string{"brolang", "app", "-o", "out", "--link", "native.c"})
_, legacy_library_path := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library-path", "vendor/lib"})
_, legacy_library := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library", "thing"})
testing.expect(t, !unknown_valid)
testing.expect(t, !incomplete_valid)
testing.expect(t, !duplicate_output_valid)
testing.expect(t, !duplicate_root_valid)
testing.expect(t, !duplicate_empty_output_valid)
testing.expect(t, !invalid_target)
testing.expect(t, !legacy_link)
testing.expect(t, !legacy_library_path)
testing.expect(t, !legacy_library)
}
@(test)
translate_c_helpers_resolve_zig_libc_headers :: proc(t: ^testing.T) {
lib_dir, parsed := zig_lib_dir_from_env_output(".{\n .lib_dir = \"/zig/lib\",\n}\n")
defer delete(lib_dir)
header, resolved := resolve_translate_c_header("native.h", []string{"examples/interop/header/include"})
defer delete(header)
testing.expect(t, is_translate_c_command("--translate-c"))
testing.expect(t, is_translate_c_command("translate-c"))
testing.expect(t, parsed)
testing.expect_value(t, lib_dir, "/zig/lib")
testing.expect(t, resolved)
testing.expect(t, strings.has_suffix(header, "examples/interop/header/include/native.h"))
}
@(test)
parser_accepts_bare_and_aliased_imports :: proc(t: ^testing.T) {
text := `import
"../math"
other :: import "../math"
escaped :: import "dir\"name\\tail"
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.imports), 3)
testing.expect_value(t, symbol.resolve(&symbols, module.imports[0].alias), "")
testing.expect_value(t, module.imports[0].path, "../math")
testing.expect_value(t, symbol.resolve(&symbols, module.imports[1].alias), "other")
testing.expect_value(t, module.imports[2].path, "dir\"name\\tail")
}
@(test)
parser_accepts_chained_field_access :: proc(t: ^testing.T) {
text := `main func() void {
_ = first.second.value
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
lexer_diagnoses_invalid_import_strings :: proc(t: ^testing.T) {
text := "import \"bad\\q\"\nimport \"unterminated\n"
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
testing.expect_value(t, len(diagnostics.items), 2)
}
@(test)
package_loader_discovers_lexical_immediate_source_files :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load("examples/packages/basic/app", &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.packages), 2)
testing.expect_value(t, len(module.files), 3)
testing.expect(t, strings.has_suffix(sources.items[module.files[0].source].path, "/main.bro"))
testing.expect(t, strings.has_suffix(sources.items[module.files[1].source].path, "/value.hon"))
testing.expect(t, strings.has_suffix(sources.items[module.files[2].source].path, "/math.bro"))
}
@(test)
multi_source_diagnostics_report_the_originating_file :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load("examples/packages/file_local/app", &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
found := false
for _, diagnostic_index in diagnostics.items {
message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index))
if strings.contains(message, "--> ") && strings.contains(message, "/b.bro:2:9") &&
strings.contains(message, "unknown symbol 'math'") {
found = true
}
delete(message)
}
testing.expect(t, found)
}
@(test)
semantic_lookup_diagnoses_wrong_declaration_kinds :: proc(t: ^testing.T) {
text := `value :: 1
give func() i8 {
return 1
}
main func() void {
_ = value()
_ = give
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_global := false
found_old_function_error := false
for diagnostic in diagnostics.items {
found_global = found_global || strings.contains(diagnostic.message, "'value' is a global, not a function")
found_old_function_error =
found_old_function_error ||
strings.contains(diagnostic.message, "'give' is a function, not a global value")
}
testing.expect(t, found_global)
testing.expect(t, !found_old_function_error)
}
@(test)
function_signature_context_infers_enum_literals :: proc(t: ^testing.T) {
text := `TokenKind :: enum {
ident
double_colon
newline
}
ParseError :: enum {
unexpected_token
}
parse func() void ! ParseError {
try expect(.ident)
try expect(.double_colon)
try expect(.newline)
}
expect func(kind TokenKind) void ! ParseError {
if kind == .newline {
return .unexpected_token
}
}
main func() void {
parse() catch |_| {}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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)
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 null"))
testing.expect(t, strings.contains(llvm_text, "orelse_fallback"))
testing.expect(t, strings.contains(llvm_text, "orelse_some"))
}
@(test)
string_literals_preserve_static_length_and_sentinel_through_pointer_views :: proc(t: ^testing.T) {
text := `take_sentinel_pointer func(_ [*;0]u8) void {}
take_mut_sentinel_pointer func(_ [*;0]mut u8) void {}
take_pointer func(_ *u8) void {}
take_sentinel_slice func(_ [;0]u8) void {}
take_mut_sentinel_slice func(_ [;0]mut u8) void {}
take_slice func(_ []u8) void {}
take_c_string c_func(value *c_char) c_int
take_c_sentinel c_func(value [*;0]c_char) c_int
main func() void {
text :: "hello"
values [2;0]mut u8 = [1, 2]
pointer :: &values
_ = text.len
_ = text.ptr
_ = text[0]
_ = text[1..]
_ = pointer.len
_ = pointer.ptr
_ = pointer[0]
_ = pointer[1..]
offset [*;0]u8 :: text.ptr + 1
suffix [*;0]u8 :: text[1..].ptr
middle []u8 :: text[1..3]
_ = offset
_ = suffix
_ = middle
take_sentinel_pointer(text)
take_pointer(text)
take_sentinel_slice(text)
take_slice(text)
take_mut_sentinel_pointer(pointer)
take_mut_sentinel_slice(pointer)
take_sentinel_pointer(pointer)
take_sentinel_slice(pointer)
_ = take_c_string(text)
_ = take_c_sentinel(text)
_ = take_c_string(text.ptr)
_ = take_c_sentinel(text.ptr)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
string_type := types.INVALID
decays := 0
for expr in hir_module.exprs {
if expr.kind == .String {
string_type = expr.type
}
if expr.kind == .Decay_Array_Pointer {
decays += 1
}
}
pointer, array, string_ok := types.array_pointer(string_type, &hir_module.types)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, string_ok && !pointer.mutable && array.child == types.U8 &&
array.count == 5 && array.has_sentinel && array.sentinel == 0)
testing.expect(t, decays >= 6)
testing.expect(t, strings.contains(llvm_text, "@bro.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\""))
testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_string(ptr)"))
testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_sentinel(ptr)"))
}
@(test)
multiline_strings_join_lines_and_strip_indentation :: proc(t: ^testing.T) {
// Source written double-quoted (not a raw `...` literal) because the
// backtick is the multi-line string marker. Covers basic join, the
// trailing-newline form, a blank line in the middle, and value-on-next-line.
text := "main func() void {\n" +
"\tbasic ::\n\t\t`a\n\t\t`b\n" +
"\ttrailing ::\n\t\t`hello\n\t\t`world\n\t\t`\n" +
"\tgapped ::\n\t\t`x\n\t\t`\n\t\t`y\n" +
"\t_ = basic\n\t_ = trailing\n\t_ = gapped\n}\n"
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(ast_module.strings), 3)
testing.expect_value(t, ast_module.strings[0], "a\nb")
testing.expect_value(t, ast_module.strings[1], "hello\nworld\n")
testing.expect_value(t, ast_module.strings[2], "x\n\ny")
// A multi-line string is an ordinary string literal: @[N;0]u8.
string_type := types.INVALID
for expr in hir_module.exprs {
if expr.kind == .String {
string_type = expr.type
break
}
}
pointer, array, string_ok := types.array_pointer(string_type, &hir_module.types)
testing.expect(t, string_ok && !pointer.mutable && array.child == types.U8 &&
array.has_sentinel && array.sentinel == 0)
}
@(test)
array_pointer_and_c_string_coercion_restrictions_are_diagnosed :: proc(t: ^testing.T) {
text := `take_c_string c_func(value *c_char) c_int
take_mut_c_string c_func(value *mut c_char) c_int
take_pointer func(value *u8) void {}
take_mut_pointer func(value *mut u8) void {}
take_slice func(value []u8) void {}
bad_sentinel func(value [*;256]u8) void {}
main func() void {
values [1;0]mut u8 = [1]
_ = values.ptr
take_pointer(values)
take_slice(values)
ordinary *u8 :: "hello"
nonzero [1;'\n']mut u8 = [1]
take_pointer("hello"[1..])
_ = take_c_string(ordinary)
_ = take_c_string((&nonzero).ptr)
_ = take_c_string(1)
take_mut_pointer("hello")
_ = take_mut_c_string("hello")
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
conversion_errors := 0
array_ptr_error := false
sentinel_error := false
for diagnostic in diagnostics.items {
conversion_errors += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0
array_ptr_error = array_ptr_error ||
strings.contains(diagnostic.message, "arrays do not expose '.ptr'")
sentinel_error = sentinel_error ||
strings.contains(diagnostic.message, "sentinel value does not fit array, slice, or pointer")
}
testing.expect_value(t, conversion_errors, 8)
testing.expect(t, array_ptr_error)
testing.expect(t, sentinel_error)
}
@(test)
immutable_pointer_and_slice_bindings_preserve_mutable_pointees :: proc(t: ^testing.T) {
text := `main func() void {
values [2]mut u8 = [1, 2]
pointer *mut u8 :: (&values).ptr
slice []mut u8 :: values[0..]
pointer[0] = 3
slice[1] = 4
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
many_item_pointer_slices_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-pointer-slices"
main_path := "/tmp/brolang-test-pointer-slices/main.bro"
output := "/tmp/brolang-test-pointer-slices-output"
text := `main func() i32 {
values [4]mut i32 = [3, 4, 5, 6]
pointer *mut i32 :: (&values).ptr
const_pointer *i32 :: pointer
all []mut i32 :: pointer[..4]
middle []mut i32 :: pointer[1..3]
readonly []i32 :: const_pointer[..2]
if (all.len != 4) return 1
if (middle.len != 2) return 2
all[0] = 10
if (readonly[0] != 10) return 3
if (middle.ptr[0] != 4) return 4
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
many_item_pointer_slices_require_end_bound :: proc(t: ^testing.T) {
text := `main func() void {
values [2]i32 = [1, 2]
pointer *i32 :: (&values).ptr
_ = pointer[..]
_ = pointer[1..]
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
end_bound_errors := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "many-item pointer slicing requires an explicit end bound") {
end_bound_errors += 1
}
}
testing.expect_value(t, end_bound_errors, 2)
}
@(test)
layout_builtins_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-layout-builtins"
main_path := "/tmp/brolang-test-layout-builtins/main.bro"
output := "/tmp/brolang-test-layout-builtins-output"
text := `Point :: struct {
x i32
y u8
}
Opaque :: opaque
Color :: enum {
red
blue
}
UserID :: distinct u32
SIZE_GLOBAL :: sizeof!(i32)
needs_usize func(value usize) usize {
return value
}
buffer func($T type) [sizeof!(T)]u8 {
data [sizeof!(T)]u8 = undefined
return data
}
main func() i32 {
bytes [_]u8 :: buffer(i32)
if (needs_usize(SIZE_GLOBAL) != 4) return 1
if (bytes.len != 4) return 2
if (sizeof!([3]u8) != 3) return 3
if (sizeof!([]u8) != 16) return 4
if (alignof!([]u8) != 8) return 5
if (sizeof!(*anyopaque) != 8) return 6
if (sizeof!(?*i32) != 8) return 7
if (sizeof!(*Opaque) != 8) return 8
if (sizeof!(Color) != 2) return 9
if (alignof!(Color) != 2) return 10
if (sizeof!(Point) != 8) return 11
if (alignof!(Point) != 4) return 12
if (sizeof!(UserID) != 4) return 13
if (alignof!(UserID) != 4) return 14
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
integer_bound_builtins_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-integer-bounds"
main_path := "/tmp/brolang-test-integer-bounds/main.bro"
output := "/tmp/brolang-test-integer-bounds-output"
text := `MAX_U64 u64 :: maxval!(u64)
Signed :: distinct i16
Unsigned :: distinct u16
Inner :: distinct u8
Outer :: distinct Inner
maximum func($T type) T {
return maxval!(T)
}
main func() i32 {
if (minval!(i8) != -128) return 1
if (maxval!(i8) != 127) return 2
if (minval!(u8) != 0) return 3
if (maxval!(u8) != 255) return 4
if (minval!(isize) != -9223372036854775808) return 5
if (maxval!(usize) != 18446744073709551615) return 6
if (MAX_U64 != 18446744073709551615) return 7
if (maximum(u16) != 65535) return 8
if (minval!(c_int) != -2147483648) return 9
if (maxval!(c_ulong) != 18446744073709551615) return 10
signed_min Signed = minval!(Signed)
unsigned_max Unsigned = maxval!(Unsigned)
nested_max Outer = maxval!(Outer)
generic_min Signed = maximum(Signed)
generic_nested Outer = maximum(Outer)
if i16(signed_min) != -32768 { return 11 }
if u16(unsigned_max) != 65535 { return 12 }
if u8(nested_max) != 255 { return 13 }
if i16(generic_min) != 32767 { return 14 }
if u8(generic_nested) != 255 { return 15 }
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
integer_bound_builtins_reject_invalid_targets :: proc(t: ^testing.T) {
text := `BadFloat :: distinct f32
BadBool :: distinct bool
BadAggregate :: distinct [2]u8
Choice :: enum { one }
main func() void {
_ = minval!()
_ = maxval!(u8, u16)
_ = minval!(1)
_ = maxval!(int)
_ = maxval!(uint)
_ = maxval!(f32)
_ = maxval!(bool)
_ = maxval!(BadFloat)
_ = maxval!(BadBool)
_ = maxval!(BadAggregate)
_ = maxval!(Choice)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
bad_arity := 0
bad_type := false
bad_target := 0
for diagnostic in diagnostics.items {
bad_arity += 1 if strings.contains(diagnostic.message, "expects 1 argument") else 0
bad_type = bad_type || strings.contains(diagnostic.message, "integer bound target must be a type")
bad_target += 1 if strings.contains(diagnostic.message, "integer bound target must be a concrete integer type") else 0
}
testing.expect_value(t, bad_arity, 2)
testing.expect(t, bad_type)
testing.expect_value(t, bad_target, 8)
}
@(test)
layout_builtins_reject_unsized_targets :: proc(t: ^testing.T) {
text := `Opaque :: opaque
Fn :: alias func() void
main func() void {
_ = sizeof!(void)
_ = alignof!(anyopaque)
_ = sizeof!(Fn)
_ = sizeof!(Opaque)
_ = alignof!(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
bad_layout_targets := 0
target_type_error := false
for diagnostic in diagnostics.items {
bad_layout_targets += 1 if strings.contains(diagnostic.message, "layout target must be a sized runtime value type") else 0
target_type_error = target_type_error || strings.contains(diagnostic.message, "layout target must be a type")
}
testing.expect_value(t, bad_layout_targets, 4)
testing.expect(t, target_type_error)
}
@(test)
slicing_an_array_variable_takes_its_address_implicitly :: proc(t: ^testing.T) {
// Milestone 10: `arr[a..b]` on an array variable slices without an explicit
// `&`. The slice operand must be a pointer to the array (getelementptr off a
// `ptr`), not the array value.
text := `sink func(s []i32) i32 {
return s[0]
}
main func() i32 {
arr [4]i32 = [10, 20, 30, 40]
full :: sink(arr[..])
part :: sink(arr[1..3])
return full + part
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr"))
}
@(test)
slicing_an_array_rvalue_materializes_a_temporary :: proc(t: ^testing.T) {
// The checker accepts slicing a non-location array (here, a by-value array
// return). Lowering must store it into a temporary and slice that address;
// otherwise the slice operand is an array value, which is an invalid pointer.
text := `make_arr func() [4]i32 {
return [1, 2, 3, 4]
}
sink func(s []i32) i32 {
return s[0]
}
main func() i32 {
return sink(make_arr()[0..])
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
// The materialization store of the array rvalue into its temporary.
testing.expect(t, strings.contains(llvm_text, "store [4 x i32]"))
testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr"))
}
@(test)
field_and_index_access_on_an_rvalue_aggregate_materializes_it :: proc(t: ^testing.T) {
// A by-value struct return is a temporary with no address. Reading a field,
// slicing an array field, and taking its address must spill it into a
// temporary and address that, rather than addressing the aggregate value.
text := `Box :: struct {
score i32
data [4]i32
}
make_box func() Box {
return Box { score = 7, data = [1, 2, 3, 4] }
}
sink func(s []i32) i32 {
return s[0]
}
main func() i32 {
s :: make_box().score
v :: sink(make_box().data[0..])
p :: &make_box().data
return s + v + p^[1]
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
// The rvalue Box is spilled into a stack temporary (structs render as
// %bro.type.N), then stored, then its fields are addressed off a `ptr`.
// A regression addresses the aggregate value directly, which llc rejects.
testing.expect(t, strings.contains(llvm_text, "alloca %bro.type."))
testing.expect(t, strings.contains(llvm_text, "store %bro.type."))
testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr"))
}
@(test)
c_variadic_calls_promote_extras_and_emit_variadic_llvm :: proc(t: ^testing.T) {
text := `variadic c_func(tag c_int, ...) c_int
zero c_func(...) void
main func() void {
narrow i8 :: -2
unsigned u16 :: 3
float_value f32 :: 4.0
c_float_value c_float :: 5.0
pointer *u8 :: "ok".ptr
nullable ?*u8 :: pointer
zero(pointer)
_ = variadic(7, narrow, unsigned, float_value, c_float_value, pointer, nullable)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
promotions := 0
for expr in hir_module.exprs {
if expr.kind == .C_Vararg_Promote {
promotions += 1
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, promotions, 4)
found_hir_variadic := false
for function in hir_module.functions {
found_hir_variadic = found_hir_variadic || function.variadic
}
found_ir_variadic := false
for function in ir_module.functions {
found_ir_variadic = found_ir_variadic || function.variadic
}
testing.expect(t, found_hir_variadic)
testing.expect(t, found_ir_variadic)
testing.expect(t, strings.contains(llvm_text, "declare i32 @variadic(i32, ...)"))
testing.expect(t, strings.contains(llvm_text, "declare void @zero(...)"))
testing.expect(t, strings.contains(llvm_text, "sext i8"))
testing.expect(t, strings.contains(llvm_text, "zext i16"))
testing.expect(t, strings.contains(llvm_text, "fpext float"))
testing.expect(t, strings.contains(llvm_text, "call void (...) @zero(ptr"))
testing.expect(t, strings.contains(llvm_text, "call i32 (i32, ...) @variadic(i32 7, i32"))
testing.expect(t, strings.contains(llvm_text, "double"))
testing.expect(t, strings.contains(llvm_text, "ptr"))
}
@(test)
c_variadic_restrictions_and_extra_argument_types_are_diagnosed :: proc(t: ^testing.T) {
text := `Record :: c_struct {
value c_int
}
foreign c_func(...) void
requires c_func(value c_int, ...) void
native func(...) void
bodyful c_func(...) void {}
main func() void {
values [1]u8 :: [1]
record Record :: Record { value = 1 }
foreign(values)
foreign(record)
requires()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
restricted := 0
found_extra := false
found_arity := false
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "must be a bodyless 'c_func' declaration") {
restricted += 1
}
found_extra = found_extra || strings.contains(diagnostic.message, "C variadic argument must be a concrete scalar or pointer")
found_arity = found_arity || strings.contains(diagnostic.message, "expects at least 1 arguments")
}
testing.expect_value(t, restricted, 2)
testing.expect(t, found_extra)
testing.expect(t, found_arity)
}
@(test)
variadicness_is_part_of_c_function_signature_compatibility :: proc(t: ^testing.T) {
fixed := ast.Function{result=types.C_INT}
variadic := ast.Function{result=types.C_INT, variadic=true}
testing.expect(t, !checker.function_signatures_equal(fixed, variadic))
testing.expect(t, !loader.function_signatures_equal(fixed, nil, types.C_INT, true))
}
@(test)
c_structs_are_by_value_and_bodyless_c_struct_uses_opaque :: proc(t: ^testing.T) {
text := `Defined :: c_struct {
value c_int
}
Opaque :: opaque
Bodyless :: c_struct
Empty :: c_struct {}
Bad :: c_struct {
values []i32
}
read c_func(value @Defined) c_int
pass c_func(value Defined) Defined
bad_opaque c_func(value Opaque) void
main func() void {
_ = pass(Defined { value = 1 })
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_opaque := false
found_bodyless := false
found_bad_layout := false
found_empty := false
for diagnostic in diagnostics.items {
found_opaque = found_opaque || strings.contains(diagnostic.message, "cannot be passed by value")
found_bodyless = found_bodyless || strings.contains(diagnostic.message, "use 'opaque'")
found_bad_layout = found_bad_layout || strings.contains(diagnostic.message, "C-layout-compatible")
found_empty = found_empty || strings.contains(diagnostic.message, "at least one field")
}
testing.expect(t, found_opaque)
testing.expect(t, found_bodyless)
testing.expect(t, found_bad_layout)
testing.expect(t, found_empty)
}
@(test)
opaque_anyopaque_and_ptrcast_compile_and_lower :: proc(t: ^testing.T) {
text := `Handle :: opaque
take func(_ ?*mut anyopaque) void {}
use_handle func(_ ?@mut Handle) void {}
main func() void {
values [2]mut u8 = [1, 2]
raw ?*mut anyopaque = (&values).ptr
bytes ?*mut u8 = ptrcast!(u8, raw)
take(bytes)
if bytes |p| {
p[1] = 5
}
one u8 = 1
single ?@mut anyopaque = &one
typed ?@mut u8 = ptrcast!(u8, single)
if typed |p| {
p^ = 2
}
handle ?@mut Handle = null
use_handle(handle)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
hir_casts := 0
for expr in hir_module.exprs {
hir_casts += 1 if expr.kind == .Pointer_Cast else 0
}
ir_casts := 0
for function in ir_module.functions {
for instruction in function.instructions {
ir_casts += 1 if instruction.op == .Pointer_Cast else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, hir_casts, 2)
testing.expect_value(t, ir_casts, 2)
}
@(test)
anyopaque_by_value_and_invalid_ptrcasts_are_rejected :: proc(t: ^testing.T) {
text := `Callback :: alias c_func() void
main func() void {
raw ?*mut anyopaque = null
value anyopaque = undefined
_ = ptrcast!(void, raw)
_ = ptrcast!(anyopaque, raw)
_ = ptrcast!(Callback, raw)
_ = ptrcast!(u8, 1)
_ = ptrcast!(1, raw)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_by_value := false
found_bad_target := false
found_bad_operand := false
found_target_type := false
for diagnostic in diagnostics.items {
found_by_value = found_by_value || strings.contains(diagnostic.message, "could not infer a concrete type")
found_bad_target = found_bad_target || strings.contains(diagnostic.message, "ptrcast! target must be a sized runtime object type")
found_bad_operand = found_bad_operand || strings.contains(diagnostic.message, "ptrcast! operand must be a pointer")
found_target_type = found_target_type || strings.contains(diagnostic.message, "ptrcast! target must be a type")
}
testing.expect(t, found_by_value)
testing.expect(t, found_bad_target)
testing.expect(t, found_bad_operand)
testing.expect(t, found_target_type)
}
@(test)
constcast_restores_pointer_and_slice_mutability :: proc(t: ^testing.T) {
text := `main func() i32 {
values [2]mut u8 = [1, 2]
immutable_slice []u8 = values[..]
mutable_slice []mut u8 = constcast!(immutable_slice)
mutable_slice[0] = 3
immutable_many *u8 = immutable_slice.ptr
mutable_many *mut u8 = constcast!(immutable_many)
mutable_many[1] = 4
number i32 = 5
immutable_single @i32 = &number
mutable_single @mut i32 = constcast!(immutable_single)
mutable_single^ = 6
maybe ?@i32 = immutable_single
mutable_maybe ?@mut i32 = constcast!(maybe)
if mutable_maybe |pointer| { pointer^ = 7 }
already_mutable []mut u8 = constcast!(mutable_slice)
if already_mutable[0] != 3 or already_mutable[1] != 4 or number != 7 { return 1 }
return 0
}
`
source_file := source.Source{path="constcast.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
hir_casts, ir_casts := 0, 0
for expr in hir_module.exprs {
hir_casts += 1 if expr.kind == .Const_Cast else 0
}
for function in ir_module.functions {
for instruction in function.instructions {
ir_casts += 1 if instruction.op == .Const_Cast else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, hir_casts, 5)
testing.expect_value(t, ir_casts, 5)
directory := "/tmp/brolang-test-constcast"
main_path := "/tmp/brolang-test-constcast/main.bro"
output := "/tmp/brolang-test-constcast-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
invalid_constcasts_are_rejected :: proc(t: ^testing.T) {
text := `main func() void {
values [2]mut u8 = [1, 2]
_ = constcast!()
_ = constcast!(1, 2)
_ = constcast!(1)
_ = constcast!(values)
}
`
source_file := source.Source{path="invalid_constcast.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
bad_arity, bad_operand := false, false
for diagnostic in diagnostics.items {
bad_arity = bad_arity || strings.contains(diagnostic.message, "constcast! expects 1 argument")
bad_operand = bad_operand || strings.contains(diagnostic.message, "constcast! operand must be a pointer, optional pointer, or slice")
}
testing.expect(t, bad_arity && bad_operand)
}
@(test)
immutable_allocations_can_be_freed_and_constcast_keeps_slice_bounds_checks :: proc(t: ^testing.T) {
free_text := `mem :: import "@std/mem"
main func() i32 {
memory []mut u8 :: mem.alloc(u8, mem.c_allocator, 4) catch |_| { return 1 }
memory[0] = 42
immutable []u8 = memory
mem.free(mem.c_allocator, immutable)
empty []u8 = mem.empty(u8)
mem.free(mem.c_allocator, empty)
return 0
}
`
directory := "/tmp/brolang-test-immutable-free"
main_path := "/tmp/brolang-test-immutable-free/main.bro"
output := "/tmp/brolang-test-immutable-free-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)free_text))
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".",
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
bounds_text := `main func() void {
values [2]mut u8 = [1, 2]
immutable []u8 = values[..]
mutable []mut u8 = constcast!(immutable)
_ = mutable[mutable.len]
}
`
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)bounds_text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
bounds_state, stdout, stderr, err := os2.process_exec(
os2.Process_Desc{command=[]string{output}}, context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect(t, err == nil)
testing.expect(t, !bounds_state.success)
testing.expect(t, strings.contains(string(stderr), "index out of bounds"))
}
@(test)
old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) {
text := `main func() void {
_ = ptr_cast(1, 1)
_ = size_of(1)
_ = align_of(1)
_ = min_value(1)
_ = max_value(1)
_ = div_trunc(1, 1)
_ = div_floor(1, 1)
_ = div_exact(1, 1)
_ = div_ceil(1, 1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
names := [?]string{
"ptr_cast", "size_of", "align_of", "min_value", "max_value",
"div_trunc", "div_floor", "div_exact", "div_ceil",
}
found := [len(names)]bool{}
for diagnostic in diagnostics.items {
if !strings.contains(diagnostic.message, "unknown symbol") {
continue
}
for name, index in names {
found[index] = found[index] || strings.contains(diagnostic.message, name)
}
}
for value in found {
testing.expect(t, value)
}
}
@(test)
bare_intrinsic_names_are_available_to_user_functions :: proc(t: ^testing.T) {
text := `memory :: import "./memory"
ptrcast func() i32 { return 1 }
sizeof func() i32 { return 2 }
alignof func() i32 { return 3 }
minval func() i32 { return 4 }
maxval func() i32 { return 5 }
divtrunc func() i32 { return 6 }
divfloor func() i32 { return 7 }
divexact func() i32 { return 8 }
divceil func() i32 { return 9 }
rem func() i32 { return 10 }
mod func() i32 { return 11 }
memcopy func() i32 { return 12 }
memset func() i32 { return 13 }
main func() i32 {
return ptrcast() + sizeof() + alignof() + minval() + maxval() +
divtrunc() + divfloor() + divexact() + divceil() + rem() + mod() +
memcopy() + memset() + memory.memcopy() + memory.memset() - 120
}
`
directory := "/tmp/brolang-test-user-intrinsic-names"
main_path := "/tmp/brolang-test-user-intrinsic-names/main.bro"
memory_directory := "/tmp/brolang-test-user-intrinsic-names/memory"
memory_path := "/tmp/brolang-test-user-intrinsic-names/memory/memory.bro"
output := "/tmp/brolang-test-user-intrinsic-names-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.make_directory(memory_directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
memory_text := `memcopy func() i32 { return 14 }
memset func() i32 { return 15 }
`
testing.expect(t, os.write_entire_file(memory_path, transmute([]byte)memory_text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
}
@(test)
intrinsic_call_diagnostics_are_precise :: proc(t: ^testing.T) {
text := `main func() void {
_ = mystery!()
_ = math.ptrcast!()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_unknown := false
found_qualified := false
for diagnostic in diagnostics.items {
found_unknown = found_unknown || diagnostic.message == "unknown intrinsic 'mystery!'"
found_qualified = found_qualified || diagnostic.message == "intrinsic calls must be unqualified"
}
testing.expect(t, found_unknown)
testing.expect(t, found_qualified)
}
@(test)
memory_intrinsics_compile_run_and_lower :: proc(t: ^testing.T) {
text := `Pair :: struct { x i32, y i32 }
calls i32 = 0
destination_view func(value []mut u8) []mut u8 {
calls += 1
return value
}
source_view func(value []u8) []u8 {
calls += 1
return value
}
compile_value func() [4]mut u8 {
source [4]mut u8 = [1, 2, 3, 4]
destination [4]mut u8 = undefined
memset!(&destination, undefined)
memcopy!(&destination, &source)
memset!(destination[1..3], 9)
memcopy!(destination[..0], destination[..0])
return destination
}
known :: $compile_value()
main func() i32 {
if known[0] != 1 or known[1] != 9 or known[2] != 9 or known[3] != 4 { return 1 }
bytes [4]mut u8 = undefined
source [4]mut u8 = [4, 3, 2, 1]
memset!(&bytes, undefined)
memcopy!(destination_view(bytes[..]), source_view(source[..]))
if calls != 2 or bytes[0] != 4 or bytes[3] != 1 { return 2 }
memset!(bytes[1..3], 7)
if bytes[1] != 7 or bytes[2] != 7 { return 3 }
wide [2]mut i32 = undefined
memset!(&wide, 42)
if wide[0] != 42 or wide[1] != 42 { return 4 }
pairs [2]mut Pair = undefined
pair_source [2]mut Pair = [Pair{x = 1, y = 2}, Pair{x = 3, y = 4}]
memcopy!(&pairs, &pair_source)
memset!(pairs[1..], Pair{x = 7, y = 8})
if pairs[0].x != 1 or pairs[1].y != 8 { return 5 }
memcopy!(bytes[..0], bytes[..0])
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)
copy_count, set_count := 0, 0
for function in ir_module.functions {
for instruction in function.instructions {
copy_count += 1 if instruction.op == .Mem_Copy else 0
set_count += 1 if instruction.op == .Mem_Set else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, copy_count >= 3)
testing.expect(t, set_count >= 3)
testing.expect(t, strings.contains(llvm_text, "memcopy_len_ok"))
testing.expect(t, strings.contains(llvm_text, "memcopy_size_ok"))
testing.expect(t, strings.contains(llvm_text, "memcopy_disjoint"))
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64"))
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memset.p0.i64"))
testing.expect(t, strings.contains(llvm_text, "memset_loop"))
directory := "/tmp/brolang-test-memory-intrinsics"
main_path := "/tmp/brolang-test-memory-intrinsics/main.bro"
output := "/tmp/brolang-test-memory-intrinsics-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
memory_intrinsic_diagnostics_are_precise :: proc(t: ^testing.T) {
text := `bad_length func() [1]mut u8 {
destination [1]mut u8 = undefined
source [2]mut u8 = [1, 2]
memcopy!(&destination, &source)
return destination
}
bad_overlap func() [3]mut u8 {
items [3]mut u8 = [1, 2, 3]
memcopy!(items[..2], items[1..])
return items
}
bad_undefined func() u8 {
items [1]mut u8 = [1]
memset!(&items, undefined)
return items[0]
}
length_value :: $bad_length()
overlap_value :: $bad_overlap()
undefined_value :: $bad_undefined()
main func() void {
immutable [2]u8 = [1, 2]
mutable [2]mut u8 = undefined
wide [2]mut u16 = undefined
memcopy!(&immutable, &mutable)
memcopy!(&mutable, &wide)
memcopy!(1, 2)
memset!(1, 0)
memcopy!()
memset!(&mutable)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
expected := [?]string{
"memcopy! source and destination lengths differ",
"memcopy! source and destination overlap",
"cannot read an undefined value at comptime",
"memcopy! destination must be a mutable slice or mutable pointer-to-array",
"memcopy! source and destination element types must match",
"memset! destination must be a mutable slice or mutable pointer-to-array",
"memcopy! expects 2 arguments, got 0",
"memset! expects 2 arguments, got 1",
}
found: [len(expected)]bool
for diagnostic in diagnostics.items {
for message, index in expected {
found[index] = found[index] || strings.contains(diagnostic.message, message)
}
}
for present in found {
testing.expect(t, present)
}
}
@(test)
memory_intrinsic_runtime_guards_trap :: proc(t: ^testing.T) {
cases := [?]struct {
directory, output, text: string,
}{
{
"/tmp/brolang-test-memcopy-length-trap",
"/tmp/brolang-test-memcopy-length-trap-output",
`copy func(destination []mut u8, source []u8) void { memcopy!(destination, source) }
main func() void {
destination [2]mut u8 = undefined
source [3]mut u8 = [1, 2, 3]
copy(destination[..], source[..])
}
`,
},
{
"/tmp/brolang-test-memcopy-overlap-trap",
"/tmp/brolang-test-memcopy-overlap-trap-output",
`copy func(destination []mut u8, source []u8) void { memcopy!(destination, source) }
main func() void {
items [4]mut u8 = [1, 2, 3, 4]
copy(items[1..], items[..3])
}
`,
},
}
for test_case in cases {
main_path := fmt.tprintf("%s/main.bro", test_case.directory)
_ = os2.remove_all(test_case.directory)
defer _ = os2.remove_all(test_case.directory)
defer _ = os.remove(test_case.output)
testing.expect(t, os.make_directory(test_case.directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)test_case.text))
testing.expect_value(t, compiler_core.compile_package(test_case.directory, test_case.output), 0)
state := run_executable(test_case.output)
testing.expect(t, !state.success)
}
}
@(test)
malformed_intrinsic_calls_have_targeted_parse_diagnostics :: proc(t: ^testing.T) {
cases := [?]struct {
text, message: string,
}{
{`main func() void { _ = sizeof! }`, "expected '(' after intrinsic name"},
{`callback func() void {}
main func() void { (callback)!() }
`, "intrinsic calls require a direct name"},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
module := parser.parse(&stream, &source_file, &diagnostics)
found := false
for diagnostic in diagnostics.items {
found = found || diagnostic.message == test_case.message
}
testing.expect(t, found)
ast.destroy_module(&module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
aarch64_c_record_abi_classifies_fixed_parameters_and_results :: proc(t: ^testing.T) {
text := `Small :: c_struct {
left c_int
right c_int
}
Medium :: c_struct {
first c_int
second c_int
third c_int
}
Hfa :: c_struct {
x c_float
y c_float
}
Large :: c_struct {
first c_long
second c_long
third c_long
}
small c_func(value Small) Small
medium c_func(value Medium) Medium
hfa c_func(value Hfa) Hfa
large c_func(value Large) Large
main func() void {
_ = small(Small { left = 1, right = 2 })
_ = medium(Medium { first = 1, second = 2, third = 3 })
_ = hfa(Hfa { x = 1.0, y = 2.0 })
_ = large(Large { first = 1, second = 2, third = 3 })
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "declare i64 @small(i64)"))
testing.expect(t, strings.contains(llvm_text, "declare [2 x i64] @medium([2 x i64])"))
testing.expect(t, strings.contains(llvm_text, "@hfa([2 x float])"))
testing.expect(t, strings.contains(llvm_text, "declare void @large(ptr sret("))
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64"))
}
@(test)
invalid_foreign_declarations_are_eagerly_diagnosed_and_calls_trap :: proc(t: ^testing.T) {
text := `bad c_func(value int) int
native func() i32
main func() void {
_ = bad(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_parameter := false
found_result := false
found_native := false
for diagnostic in diagnostics.items {
found_parameter = found_parameter || strings.contains(diagnostic.message, "requires concrete parameter types")
found_result = found_result || strings.contains(diagnostic.message, "requires a concrete or void result type")
found_native = found_native || strings.contains(diagnostic.message, "must use 'c_func'")
}
testing.expect(t, found_parameter)
testing.expect(t, found_result)
testing.expect(t, found_native)
testing.expect(t, !strings.contains(llvm_text, "@bad("))
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
}
@(test)
duplicate_foreign_symbols_across_packages_are_poisoned :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load("examples/packages/foreign_duplicate/app", &sources, &diagnostics, &symbols)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
duplicate_count := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "duplicate foreign symbol 'same'") {
duplicate_count += 1
}
}
testing.expect(t, loaded)
testing.expect_value(t, duplicate_count, 2)
testing.expect(t, !strings.contains(llvm_text, "@same("))
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
}
@(test)
bodyless_root_main_recovers_as_a_trap_definition :: proc(t: ^testing.T) {
text := "main c_func() i32\n"
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "main must have a body")
}
testing.expect(t, found)
testing.expect_value(t, len(ir_module.functions), 1)
testing.expect_value(t, ir_module.functions[0].implementation, ir.Implementation.Definition)
testing.expect(t, strings.contains(llvm_text, "define i32 @main()"))
testing.expect(t, !strings.contains(llvm_text, "declare i32 @main()"))
}
@(test)
milestone_33_injects_explicit_io_provider_and_runs_std_io :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(
"examples/programs/io",
&sources,
&diagnostics,
&symbols,
project_root_path=".",
)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, hir_module.injected_main != hir.INVALID_FUNCTION)
testing.expect(t, hir_module.io_provider != hir.INVALID_FUNCTION)
testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main()"), 1)
testing.expect_value(t, count_substring_occurrences(llvm_text, "define internal fastcc i32 @bro__p0__main__"), 1)
testing.expect_value(t, count_substring_occurrences(llvm_text, "declare i64 @write("), 1)
output := "/tmp/brolang-test-io"
defer _ = os.remove(output)
status := compiler_core.compile_package(
"examples/programs/io",
output,
nil,
target.DEFAULT,
cimport.Options{},
".",
)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect_value(t, state.exit_code, 0)
testing.expect_value(t, string(stdout),
"io-ok 37 {bro}\n" +
"bounds=-128/255 -32768/65535 -2147483648/4294967295 -9223372036854775808/18446744073709551615 " +
"-9223372036854775808/18446744073709551615 -128/127 -128/255 -32768/65535 -2147483648/4294967295 " +
"-9223372036854775808/18446744073709551615 -9223372036854775808/18446744073709551615 0/0 1/1 -1/1\n",
)
}
@(test)
std_io_opens_existing_files_through_the_captured_provider :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-file-io"
main_path := "/tmp/brolang-test-file-io/main.bro"
data_path := "/tmp/brolang-test-file-io/data"
output := "/tmp/brolang-test-file-io/app"
seed := "xxxxx"
text := `io :: import "@std/io"
process :: import "@std/process"
missing_fails func(system io.Io) bool {
_ = io.open(system, "/tmp/brolang-test-file-io/missing", .read_only) catch |err| {
return err == .open_failed
}
return false
}
wrong_read_fails func(file io.File) bool {
buffer [1]mut u8 = [0]
_ = io.read(io.reader(file), buffer[..]) catch |err| {
return err == .not_open_for_reading
}
return false
}
wrong_write_fails func(file io.File) bool {
_ = io.write(io.writer(file), "x") catch |err| {
return err == .not_open_for_writing
}
return false
}
main func(init process.Init) i32 {
if !missing_fails(init.io) {
return 1
}
read_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .read_only) catch |_| {
return 2
}
buffer [5]mut u8 = [0, 0, 0, 0, 0]
count usize = io.read(io.reader(read_file), buffer[..]) catch |_| {
return 4
}
if count != 5 or buffer[0] != 'x' or !wrong_write_fails(read_file) {
return 5
}
io.close(read_file) catch |_| {
return 6
}
write_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .write_only) catch |_| {
return 7
}
if !wrong_read_fails(write_file) {
return 8
}
io.write_all(io.writer(write_file), "bro") catch |_| {
return 9
}
io.close(write_file) catch |_| {
return 10
}
read_write_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .read_write) catch |_| {
return 11
}
count = io.read(io.reader(read_write_file), buffer[..]) catch |_| {
return 12
}
if count != 5 or buffer[0] != 'b' or buffer[1] != 'r' or buffer[2] != 'o' {
return 13
}
io.write_all(io.writer(read_write_file), "!") catch |_| {
return 14
}
io.close(read_write_file) catch |_| {
return 15
}
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect(t, os.write_entire_file(data_path, transmute([]byte)seed))
status := compiler_core.compile_package(
directory,
output,
nil,
target.DEFAULT,
cimport.Options{},
".",
)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect_value(t, state.exit_code, 0)
testing.expect_value(t, len(stdout), 0)
testing.expect_value(t, len(stderr), 0)
data, ok := os.read_entire_file(data_path)
defer delete(data)
testing.expect(t, ok)
testing.expect_value(t, string(data), "broxx!")
}
@(test)
milestone_33_rejects_non_io_and_extra_main_parameters :: proc(t: ^testing.T) {
cases := [?]string{
"main func(value i32) void {}\n",
"main func(left, right i32) void {}\n",
"main func($value i32) void {}\n",
}
for text in cases {
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(
diagnostic.message,
"take no parameters or one @std/process Init",
)
}
testing.expect(t, found)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
milestone_37_rejects_direct_io_main_parameter :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-direct-io-main"
main_path := "/tmp/brolang-test-direct-io-main/main.bro"
text := `io :: import "@std/io"
main func(system io.Io) void {
_ = system
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".")
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(
diagnostic.message,
"take no parameters or one @std/process Init",
)
}
testing.expect(t, loaded)
testing.expect(t, found)
}
@(test)
milestone_37_validates_process_init_and_hidden_system_provider :: proc(t: ^testing.T) {
Case :: struct {
io_source: string,
process_source: string,
message: string,
}
cases := []Case{
{
io_source=`Io :: struct { value i32 }
system func() Io { return Io {value = 0} }
`,
process_source=`io :: import "@std/io"
Init :: struct { io io.Io }
`,
message="does not provide the required 'hide system func() Io'",
},
{
io_source=`Io :: struct { value i32 }
hide system func() Io { return Io {value = 0} }
`,
process_source=`io :: import "@std/io"
Init :: struct { io io.Io, extra i32 }
`,
message="Init must be an auto-layout record containing exactly 'io io.Io'",
},
}
for test_case, index in cases {
root := fmt.tprintf("/tmp/brolang-test-process-schema-%d", index)
app := fmt.tprintf("%s/app", root)
io_dir := fmt.tprintf("%s/std/io", root)
process_dir := fmt.tprintf("%s/std/process", root)
main_source: string = `process :: import "@std/process"
main func(init process.Init) void { _ = init }
`
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, os2.make_directory_all(app) == nil)
testing.expect(t, os2.make_directory_all(io_dir) == nil)
testing.expect(t, os2.make_directory_all(process_dir) == nil)
testing.expect(t, os.write_entire_file(
fmt.tprintf("%s/main.bro", app), transmute([]byte)main_source,
))
testing.expect(t, os.write_entire_file(
fmt.tprintf("%s/io.bro", io_dir), transmute([]byte)test_case.io_source,
))
testing.expect(t, os.write_entire_file(
fmt.tprintf("%s/process.bro", process_dir), transmute([]byte)test_case.process_source,
))
sources := source.init_store()
diagnostics := source.init_store_diagnostics(&sources)
symbols := symbol.init_table()
ast_module, loaded := loader.load(app, &sources, &diagnostics, &symbols, project_root_path=root)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.message)
}
testing.expect(t, loaded)
testing.expect(t, found)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
source.destroy_store(&sources)
}
}
@(test)
milestone_37_tuples_reflection_expand_for_and_debug_print_compile_and_run :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(
"examples/programs/tuples", &sources, &diagnostics, &symbols, project_root_path=".",
)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, !strings.contains(llvm_text, "FormatToken"))
testing.expect(t, !strings.contains(llvm_text, "parse_format"))
testing.expect(t, !strings.contains(llvm_text, "format_field_name"))
testing.expect(t, !strings.contains(llvm_text, "FieldInfo"))
testing.expect(t, !strings.contains(llvm_text, "RecordInfo"))
output := "/tmp/brolang-test-tuples"
defer _ = os.remove(output)
status := compiler_core.compile_package(
"examples/programs/tuples", output, nil, target.DEFAULT, cimport.Options{}, ".",
)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}}, context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect_value(t, state.exit_code, 0)
testing.expect_value(t, string(stdout), "")
testing.expect_value(t, string(stderr), "hello!\ntuple=40/bro, limits=-9223372036854775808/18446744073709551615")
}
@(test)
expanded_matches_and_tag_intrinsics_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-inline"
defer _ = os.remove(output)
status := compiler_core.compile_package(
"examples/programs/inline", output, nil, target.DEFAULT, cimport.Options{}, ".",
)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}}, context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect_value(t, state.exit_code, 0)
testing.expect_value(t, string(stdout), "")
testing.expect_value(t, string(stderr), "")
}
@(test)
expanded_match_and_tag_diagnostics :: proc(t: ^testing.T) {
text := `E :: enum { a, b }
Plain :: union { value i32 }
main func() void {
inline for {1} |value| { _ = value }
n i32 = 1
match n { inline |value|: _ = value }
e E = .a
match e { inline |value, tag|: _ = value }
match e {
.a, .b: {}
inline |value|: _ = value
}
match e {
inline |value|: _ = value
.b: {}
}
match e {
else: {}
inline |value|: _ = value
}
u Plain = Plain{value = 1}
_ = tag!(u)
_ = tagname!(e)
match e { inline ||: {} }
match e { inline |a, b, c|: {} }
}
`
source_file := source.Source{path="expand_diagnostics.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_subject := false
found_captures := false
found_redundant := false
found_after := false
found_missing := false
found_many := false
found_tag := false
found_tagname := false
for diagnostic in diagnostics.items {
message := diagnostic.message
found_subject = found_subject || strings.contains(message, "'inline' requires an enum or tagged-union")
found_captures = found_captures || strings.contains(message, "requires exactly one capture")
found_redundant = found_redundant || strings.contains(message, "redundant 'inline'")
found_after = found_after || strings.contains(message, "arms after 'inline' are unreachable") || strings.contains(message, "arms after 'else' are unreachable")
found_missing = found_missing || strings.contains(message, "expected an inline value capture")
found_many = found_many || strings.contains(message, "at most two captures")
found_tag = found_tag || strings.contains(message, "tag! requires a tagged-union value")
found_tagname = found_tagname || strings.contains(message, "tagname! requires a comptime-known enum value")
}
testing.expect(t, found_subject)
testing.expect(t, found_captures)
testing.expect(t, found_redundant)
testing.expect(t, found_after)
testing.expect(t, found_missing)
testing.expect(t, found_many)
testing.expect(t, found_tag)
testing.expect(t, found_tagname)
}
@(test)
milestone_37_format_errors_are_reported_at_comptime :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-format-errors"
main_path := "/tmp/brolang-test-format-errors/main.bro"
text := `io :: import "@std/io"
process :: import "@std/process"
Aggregate :: distinct [2]u8
main func(init process.Init) void {
writer io.Writer :: io.stdout(init.io)
stored :: typeinfo!(i32)
_ = stored
io.print(writer, "", 1) catch |_| {}
io.print(writer, "{s}", {1,}) catch |_| {}
io.print(writer, "{d}", {"bro",}) catch |_| {}
io.print(writer, "{d}{d}", {1,}) catch |_| {}
io.print(writer, "{d}", {1, 2}) catch |_| {}
io.print(writer, "{q}", {}) catch |_| {}
io.print(writer, "{b}", {1.5,}) catch |_| {}
io.print(writer, "{e}", {1,}) catch |_| {}
io.print(writer, "{c}", {i16(65),}) catch |_| {}
io.print(writer, "}", {}) catch |_| {}
io.print(writer, "{}", {Aggregate([1, 2]),}) catch |_| {}
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".")
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := [11]bool{}
for diagnostic in diagnostics.items {
message := diagnostic.message
found[0] = found[0] || strings.contains(message, "arguments must be a tuple")
found[1] = found[1] || strings.contains(message, "cannot implicitly convert i8 to []u8")
found[2] = found[2] || strings.contains(message, "'{d}' requires an integer or float")
found[3] = found[3] || strings.contains(message, "argument count does not match")
found[4] = found[4] || strings.contains(message, "unknown specifier")
found[5] = found[5] || strings.contains(message, "unmatched '}'")
found[6] = found[6] || strings.contains(message, "compile-time-only metadata")
found[7] = found[7] || strings.contains(message, "integer format requires an integer argument")
found[8] = found[8] || strings.contains(message, "float format requires a float argument")
found[9] = found[9] || strings.contains(message, "'{c}' requires an unsigned integer that fits in u8")
found[10] = found[10] || strings.contains(message, "io.print '{}' does not support this argument type")
}
testing.expect(t, loaded)
for present in found {
testing.expect(t, present)
}
}
@(test)
milestone_39_stable_values_and_richer_formatting_compile_and_run :: proc(t: ^testing.T) {
stable_names: [dynamic]string
defer {
for name in stable_names {
delete(name)
}
delete(stable_names)
}
for pass := 0; pass < 2; pass += 1 {
sources := source.init_store()
diagnostics := source.init_store_diagnostics(&sources)
symbols := symbol.init_table()
ast_module, loaded := loader.load(
"examples/programs/milestone_39", &sources, &diagnostics, &symbols, project_root_path=".",
)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
ir_module := lower.lower(&hir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
score_count := 0
for function in hir_module.functions {
if strings.contains(function.link_name, "bro__p0__score__ca") {
if pass == 0 {
append(&stable_names, strings.clone(function.link_name))
} else {
testing.expect_value(t, function.link_name, stable_names[score_count])
}
score_count += 1
}
}
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
// score has only comptime parameters, so both calls materialize without HIR functions.
testing.expect_value(t, score_count, 0)
testing.expect_value(t, count_substring_occurrences(llvm_text, "define internal fastcc i32 @bro__p0__read_carrier__"), 1)
testing.expect(t, !strings.contains(llvm_text, "FormatToken"))
testing.expect(t, !strings.contains(llvm_text, "parse_format"))
testing.expect(t, !strings.contains(llvm_text, "FieldInfo"))
testing.expect(t, !strings.contains(llvm_text, "EnumInfo"))
delete(llvm_text)
ir.destroy_module(&ir_module)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
source.destroy_store(&sources)
}
output := "/tmp/brolang-test-milestone-39"
defer _ = os.remove(output)
status := compiler_core.compile_package(
"examples/programs/milestone_39", output, nil, target.DEFAULT, cimport.Options{}, ".",
)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}}, context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect_value(t, state.exit_code, 0)
testing.expect_value(
t,
string(stdout),
"true -42 1.5 .running bro 2.5 1010 12 ff FF A 1.5000000000000000e+00 {} -9223372036854775808 0 inf nan 1.50000000e+00\n-42 -42 1010 12 ff FF A 1.50000000e+00 7\n",
)
testing.expect_value(t, string(stderr), "debug=.idle 2a\n")
}
@(test)
milestone_39_rejects_values_without_stable_identity :: proc(t: ^testing.T) {
text := `BadUnion :: union { number i32, flag bool }
Config :: struct { value i32 }
reject_pointer func($value @i32) void {}
reject_slice func($value []i32) void {}
reject_range func($value range) void {}
reject_union func($value BadUnion) void {}
reject_undefined func($value Config) void {}
stored i32 :: 1
items [2]i32 :: [1, 2]
main func() void {
reject_pointer(&stored)
reject_slice(items[..])
reject_range(0..3)
reject_union(BadUnion {number = 1})
reject_undefined(Config {value = undefined})
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "comptime argument has no stable comptime identity") {
found += 1
}
}
testing.expect(t, found >= 5)
}
@(test)
comptime_function_parameters_specialize_and_lower_directly :: proc(t: ^testing.T) {
text := `Callback_Config :: struct { call func(value i32) i32 }
Callback_Choice :: union(enum) {
call func(value i32) i32
empty void
}
increment func(value i32) i32 { return value + 1 }
decrement func(value i32) i32 { return value - 1 }
external c_func(value i32) i32
apply func($callback func(value i32) i32, value i32) i32 {
return callback(value)
}
apply_c func($callback c_func(value i32) i32, value i32) i32 {
return callback(value)
}
apply_config func($config Callback_Config, value i32) i32 {
return config.call(value)
}
apply_array func($callbacks [2]func(value i32) i32, value i32) i32 {
return callbacks[0](value) + callbacks[1](value)
}
apply_optional func($callback ?func(value i32) i32, value i32) i32 {
return callback?(value)
}
apply_choice func($choice Callback_Choice, value i32) i32 {
return match choice {
.call |callback|: callback(value)
.empty: value
}
}
apply_pointer func($callback @func(value i32) i32, value i32) i32 {
return callback(value)
}
call_pointer func(callback @func(value i32) i32, value i32) i32 {
return callback(value)
}
materialize func($callback func(value i32) i32, value i32) i32 {
return call_pointer(callback, value)
}
main func() i32 {
a i32 :: apply(increment, 1)
b i32 :: apply(increment, 2)
c i32 :: apply(decrement, 3)
d i32 :: apply(func(value i32) i32 { return value + 2 }, 4)
e i32 :: apply_c(external, 5)
f i32 :: apply_config(Callback_Config {call = increment}, 6)
g i32 :: apply_array([increment, decrement], 7)
h i32 :: apply_optional(increment, 8)
i i32 :: apply_choice(Callback_Choice {call = increment}, 9)
j i32 :: apply_pointer(increment, 10)
k i32 :: materialize(increment, 11)
return a + b + c + d + e + f + g + h + i + j + k
}
`
stable_names: [dynamic]string
defer {
for name in stable_names {
delete(name)
}
delete(stable_names)
}
for pass := 0; pass < 2; pass += 1 {
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
ir_module := lower.lower(&hir_module)
apply_count := 0
callback_specializations := 0
found_materialization := false
for function in ir_module.functions {
plain_apply := strings.contains(function.link_name, "bro__p0__apply__")
callback_specialization := plain_apply ||
strings.contains(function.link_name, "bro__p0__apply_c__") ||
strings.contains(function.link_name, "bro__p0__apply_config__") ||
strings.contains(function.link_name, "bro__p0__apply_array__") ||
strings.contains(function.link_name, "bro__p0__apply_optional__") ||
strings.contains(function.link_name, "bro__p0__apply_choice__") ||
strings.contains(function.link_name, "bro__p0__apply_pointer__")
if strings.contains(function.link_name, "bro__p0__materialize__") {
for instruction in function.instructions {
found_materialization = found_materialization || instruction.op == .Function_Address
}
}
if !callback_specialization {
continue
}
if plain_apply {
apply_count += 1
}
if pass == 0 {
append(&stable_names, strings.clone(function.link_name))
} else {
testing.expect_value(t, function.link_name, stable_names[callback_specializations])
}
callback_specializations += 1
testing.expect_value(t, len(function.param_types), 1)
found_direct_call := false
for instruction in function.instructions {
testing.expect(t, instruction.op != .Function_Address)
if instruction.op == .Call {
testing.expect(t, ir.as_function(instruction.target) != ir.INVALID_FUNCTION)
found_direct_call = true
}
}
testing.expect(t, found_direct_call)
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, apply_count, 3)
testing.expect_value(t, callback_specializations, 9)
testing.expect(t, found_materialization)
ir.destroy_module(&ir_module)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
comptime_only_function_identities_reject_runtime_storage_and_abi_use :: proc(t: ^testing.T) {
text := `Callback :: alias func(value i32) i32
Config :: struct { callback Callback }
Bad_C :: c_struct { callback c_func(value i32) i32 }
increment func(value i32) i32 { return value + 1 }
bad_param func(callback Callback) i32 { return callback(1) }
bad_config func(config Config) i32 { return config.callback(1) }
bad_result func() Callback { return increment }
stored Callback = increment
main func() void {
local Callback = increment
_ = bad_result()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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_param := false
found_config := false
found_result := false
found_global := false
found_local := false
found_c_field := false
for diagnostic in diagnostics.items {
found_param = found_param || strings.contains(diagnostic.message, "parameter 'callback' has a comptime-only type")
found_config = found_config || strings.contains(diagnostic.message, "parameter 'config' has a comptime-only type")
found_result = found_result || strings.contains(diagnostic.message, "has a comptime-only result")
found_global = found_global || strings.contains(diagnostic.message, "global 'stored' has a comptime-only type")
found_local = found_local || strings.contains(diagnostic.message, "local 'local' has a comptime-only type")
found_c_field = found_c_field || strings.contains(diagnostic.message, "c_struct fields must have C-layout-compatible types")
}
testing.expect(t, found_param)
testing.expect(t, found_config)
testing.expect(t, found_result)
testing.expect(t, found_global)
testing.expect(t, found_local)
testing.expect(t, found_c_field)
}
@(test)
function_pointers_do_not_coerce_back_to_bare_identities :: proc(t: ^testing.T) {
text := `Callback :: alias func(value i32) i32
increment func(value i32) i32 { return value + 1 }
apply func($callback Callback, value i32) i32 { return callback(value) }
pointer @func(value i32) i32 :: increment
main func() i32 { return apply(pointer, 1) }
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "comptime argument")
}
testing.expect(t, found)
}
@(test)
bodyless_c_function_identity_is_not_comptime_executable :: proc(t: ^testing.T) {
text := `external c_func(value i32) i32
apply_c func($callback c_func(value i32) i32, value i32) i32 { return callback(value) }
answer :: $apply_c(external, 1)
main func() i32 { return answer }
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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, "runtime-only")
}
testing.expect(t, found)
}
@(test)
milestone_37_expand_loop_control_must_be_statically_resolvable :: proc(t: ^testing.T) {
text := `main func() void {
total i32 = 0
inline for {1, 2} |value| {
if total == 0 {
break
}
total += value
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(
diagnostic.message,
"break or continue targeting an inline loop must be compile-time-resolvable",
)
}
testing.expect(t, found)
}
@(test)
milestone_37_comptime_undefined_aggregates_support_full_initialization :: proc(t: ^testing.T) {
text := `Token :: struct {
text []u8
count usize
}
Partial :: struct { initialized i32, text []u8 }
make_tokens func() [2]mut Token {
tokens [2]mut Token = undefined
tokens[0] = Token {text = "a", count = 1}
tokens[1].text = "bro"
tokens[1].count = 3
return tokens
}
read_initialized_sibling func() i32 {
value Partial = undefined
value.initialized = 42
return value.initialized
}
answer :: $read_initialized_sibling()
main func() i32 {
total usize = 0
inline for make_tokens() |token| {
total += token.text.len + token.count
}
if answer != 42 or total != 8 {
return 1
}
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
milestone_37_comptime_undefined_values_cannot_be_observed :: proc(t: ^testing.T) {
text := `Bad :: struct { value i32, text []u8 }
read_scalar func() i32 {
value i32 = undefined
return value
}
return_partial func() Bad {
value Bad = undefined
value.value = 1
return value
}
take_bad func(value Bad) i32 {
return value.value
}
pass_partial func() i32 {
value Bad = undefined
value.value = 1
return take_bad(value)
}
bad_scalar :: $read_scalar()
bad_record :: $return_partial()
bad_argument :: $pass_partial()
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_read := false
found_return := false
found_pass := false
for diagnostic in diagnostics.items {
found_read = found_read || strings.contains(diagnostic.message, "cannot read an undefined value at comptime")
found_return = found_return || strings.contains(diagnostic.message, "comptime function returned an undefined value")
found_pass = found_pass || strings.contains(diagnostic.message, "cannot pass an undefined value at comptime")
}
testing.expect(t, found_read)
testing.expect(t, found_return)
testing.expect(t, found_pass)
}
@(test)
milestone_37_expand_expansions_keep_distinct_call_resolutions :: proc(t: ^testing.T) {
text := `identity func($T type, value T) T {
return value
}
main func() i32 {
total i64 = 0
inline for {{i8(1), i16(2)}, {i32(3), i64(4)}} |row| {
inline for row |value| {
total += i64(identity(value))
}
}
return i32(total - 10)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
identity_symbol := symbol.intern(&symbols, "identity")
specializations := 0
for function in hir_module.functions {
specializations += 1 if function.name == identity_symbol else 0
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, specializations, 4)
}
@(test)
milestone_37_expand_control_prunes_inference_after_static_exit :: proc(t: ^testing.T) {
text := `take_i8 func(value i8) void { _ = value }
main func() void {
inline for {i8(1), "skip"} |value, index| {
if index == 1 {
continue
}
take_i8(value)
}
inline for {i8(1), "stop"} |value, index| {
if index == 1 {
break
}
take_i8(value)
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
milestone_37_expand_match_specialization_prunes_unselected_arms :: proc(t: ^testing.T) {
text := `Kind :: enum { integer, string, stop }
IntToken :: struct { kind Kind, value i8 }
StringToken :: struct { kind Kind, value []u8 }
StopToken :: struct { kind Kind, value bool }
take_i8 func(value i8) void { _ = value }
main func() void {
inline for {
IntToken {kind = .integer, value = 1},
StringToken {kind = .string, value = "ok"},
StopToken {kind = .stop, value = false},
} |token| {
match token.kind {
.integer: take_i8(token.value)
.string: {
_ = token.value.len
continue
}
.stop: break
}
}
inline for {i8(2), "skip", "stop"} |value, index| {
match index {
0: {}
1..=1, 7: continue
else: break
}
take_i8(value)
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
milestone_33_rejects_an_incompatible_runtime_write_declaration :: proc(t: ^testing.T) {
text := `write c_func(_ c_int, _ c_int, _ c_ulong) c_long
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(
diagnostic.message,
"external C function 'write' conflicts with the compiler runtime declaration",
)
}
testing.expect(t, found)
}
@(test)
literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) {
text := `return_i16 func() i16 {
return 1 + 2
}
take_i16 func(value i16) i16 {
return value
}
take_int func(value int) int {
return value
}
main func() void {
local i16 :: 1 + 2
_ = local
_ = 100 + (20 + 8)
_ = return_i16()
_ = take_i16(1 + 2)
_ = take_int(127 + 1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__take_int__i16"))
for function in ir_module.functions {
for instruction in function.instructions {
testing.expect(t, instruction.op != ir.Opcode.Add_Checked)
}
}
for function in hir_module.functions {
for statement_id in function.body {
statement := hir_module.statements[statement_id]
if statement.expr < 0 {
continue
}
expr := hir_module.exprs[statement.expr]
if expr.kind == .Integer {
testing.expect(t, types.equal(expr.type, types.I16))
}
if expr.kind == .Call && symbol.resolve(&symbols, function.name) == "main" && len(expr.args) > 0 {
arg := hir_module.exprs[expr.args[0]]
testing.expect_value(t, arg.kind, hir.Expr_Kind.Integer)
testing.expect(t, types.equal(arg.type, types.I16))
}
}
}
}
@(test)
runtime_arithmetic_does_not_inherit_result_context :: proc(t: ^testing.T) {
text := `widen_after_add func(value i8) i16 {
return value + 1
}
main func() void {
_ = widen_after_add(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
found := false
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) != "widen_after_add" {
continue
}
found = true
statement := hir_module.statements[function.body[0]]
widen := hir_module.exprs[statement.expr]
add := hir_module.exprs[widen.left]
testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen)
testing.expect(t, types.equal(widen.type, types.I16))
testing.expect_value(t, add.kind, hir.Expr_Kind.Add)
testing.expect(t, types.equal(add.type, types.I8))
}
testing.expect(t, found)
}
@(test)
parser_recovers_after_invalid_tokens :: proc(t: ^testing.T) {
text := `broken @ declaration
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect(t, len(diagnostics.items) > 0)
testing.expect_value(t, len(module.functions), 1)
testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "main")
}
@(test)
return_sink_and_unconsumed_values_have_distinct_hir :: proc(t: ^testing.T) {
text := `give func() i8 {
return 1
}
done func() void {
return
}
main func() void {
done()
_ = give()
give()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
done_id, main_id := -1, -1
for function, id in hir_module.functions {
if symbol.resolve(&symbols, function.name) == "done" {
done_id = id
} else if symbol.resolve(&symbols, function.name) == "main" {
main_id = id
}
}
testing.expect(t, done_id >= 0)
testing.expect(t, main_id >= 0)
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].kind, hir.Stmt_Kind.Return)
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].expr, hir.INVALID_EXPR)
main := hir_module.functions[main_id]
testing.expect_value(t, hir_module.statements[main.body[0]].kind, hir.Stmt_Kind.Expression)
testing.expect_value(t, hir_module.statements[main.body[1]].kind, hir.Stmt_Kind.Sink)
testing.expect_value(t, hir_module.statements[main.body[2]].kind, hir.Stmt_Kind.Trap)
}
@(test)
bare_returns_and_strict_yields :: proc(t: ^testing.T) {
text := `Failure :: enum { bad }
noop func() void {}
newline_return func() void {
return
}
inline_return func() void { return }
fallible_return func() void ! Failure { return }
split_return func() i32 {
return
1
}
old_return func() void { return _ }
missing_yield func() i32 {
value :: {
yield
1
}
return value
}
missing_labeled_yield func() i32 {
value :: block: {
yield :block
}
return value
}
void_yield func() i32 {
value :: { yield noop() }
return value
}
sink_yield func() i32 {
value :: { yield _ }
return value
}
void_context func() void {
fallible_return() catch |_| { yield 1 }
}
bad_comptime :: ${ yield noop() }
main func() void {
newline_return()
inline_return()
fallible_return() catch |_| {}
_ = split_return()
old_return()
_ = missing_yield()
_ = missing_labeled_yield()
_ = void_yield()
_ = sink_yield()
void_context()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, ast_module.statements[ast_module.functions[1].body[0]].expr, ast.INVALID_EXPR)
testing.expect_value(t, ast_module.statements[ast_module.functions[2].body[0]].expr, ast.INVALID_EXPR)
testing.expect_value(t, ast_module.statements[ast_module.functions[3].body[0]].expr, ast.INVALID_EXPR)
testing.expect_value(t, len(ast_module.functions[4].body), 2)
missing_value := false
non_void_function := false
void_yields := 0
sink_read := false
void_context := false
for diagnostic in diagnostics.items {
missing_value = missing_value || strings.contains(diagnostic.message, "'yield' must produce a value")
non_void_function = non_void_function || strings.contains(diagnostic.message, "non-void function must return a value")
if strings.contains(diagnostic.message, "'yield' expression must produce a non-void value") {
void_yields += 1
}
sink_read = sink_read || strings.contains(diagnostic.message, "'_' is a write-only sink and cannot be read")
void_context = void_context || strings.contains(diagnostic.message, "void value context must fall through instead of yielding")
}
testing.expect(t, missing_value)
testing.expect(t, non_void_function)
testing.expect(t, void_yields >= 2)
testing.expect(t, sink_read)
testing.expect(t, void_context)
}
@(test)
unused_locals_and_params_warn_without_traps :: proc(t: ^testing.T) {
text := `warn_only func(value i32, unused i32) i32 {
local i32 = 1
write_only i32 = 2
write_only = 3
consumed i32 = value
_ = consumed
return value
}
main func() void {
_ = warn_only(1, 2)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_unused_param := false
found_unused_local := false
found_write_only := false
warning_count := 0
error_count := 0
for diagnostic in diagnostics.items {
if diagnostic.severity == source.Severity.Warning {
warning_count += 1
} else {
error_count += 1
}
found_unused_param = found_unused_param || strings.contains(diagnostic.message, "unused parameter 'unused'")
found_unused_local = found_unused_local || strings.contains(diagnostic.message, "unused local 'local'")
found_write_only = found_write_only || strings.contains(diagnostic.message, "unused local 'write_only'")
testing.expect(t, !strings.contains(diagnostic.message, "unused local 'consumed'"))
testing.expect(t, !strings.contains(diagnostic.message, "unused parameter 'value'"))
}
testing.expect_value(t, warning_count, 3)
testing.expect_value(t, error_count, 0)
testing.expect(t, found_unused_param)
testing.expect(t, found_unused_local)
testing.expect(t, found_write_only)
found_function := false
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) != "warn_only" {
continue
}
found_function = true
testing.expect(t, !function.problematic)
for stmt_id in function.body {
testing.expect(t, hir_module.statements[stmt_id].kind != hir.Stmt_Kind.Trap)
}
}
testing.expect(t, found_function)
}
@(test)
generic_parameter_usage_is_source_based :: proc(t: ^testing.T) {
text := `choose func($N usize, used, unused i32) i32 {
if N > 0 {
return used
}
return 0
}
main func() void {
_ = choose(0, 1, 2)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 1)
testing.expect_value(t, diagnostics.items[0].severity, source.Severity.Warning)
testing.expect(t, strings.contains(diagnostics.items[0].message, "unused parameter 'unused'"))
testing.expect(t, !strings.contains(diagnostics.items[0].message, "unused parameter 'used'"))
}
@(test)
recursive_specialization_reaches_a_fixed_point :: proc(t: ^testing.T) {
text := `a func(value int) i32 {
return b(value)
}
b func(value int) i32 {
return a(value)
}
main func() void {
_ = a(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 3)
}
@(test)
stale_specializations_are_pruned_after_inference :: proc(t: ^testing.T) {
text := `derived :: identity(make())
wide :: delayed()
identity func(value int) int {
return value
}
make func() int {
return wide
return 1
}
delayed func() int {
return 128
}
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 4)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__identity__i16"))
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity__i8"))
}
@(test)
eager_global_calls_root_specializations :: proc(t: ^testing.T) {
text := `make func() i32 {
return 7
}
unused_native func() i32 {
return 9
}
unused_foreign c_func() i32
value i32 :: make()
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 1)
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__make("))
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__unused_native("))
testing.expect(t, !strings.contains(llvm_text, "@unused_foreign("))
}
@(test)
malformed_generic_calls_do_not_retain_specializations :: proc(t: ^testing.T) {
text := `identity func(value int) int {
return value
}
bad :: identity(1, 2)
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_arity := false
for diagnostic in diagnostics.items {
found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 arguments, got 2")
}
testing.expect(t, found_arity)
testing.expect_value(t, len(hir_module.functions), 1)
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity"))
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
}
@(test)
long_generic_call_chain_reaches_a_fixed_point :: proc(t: ^testing.T) {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
for index in 0 ..< 70 {
fmt.sbprintf(&builder, "fn%d func(value int) int ", index)
strings.write_string(&builder, "{ return ")
if index == 69 {
strings.write_string(&builder, "value")
} else {
fmt.sbprintf(&builder, "fn%d(value)", index+1)
}
strings.write_string(&builder, " }\n")
}
strings.write_string(&builder, "main func() i32 { return fn0(1) }\n")
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 71)
}
@(test)
globals_and_generic_results_reach_a_shared_fixed_point :: proc(t: ^testing.T) {
text := `derived :: identity(base)
base :: make()
identity func(value int) int {
return value
}
make func() int {
return 1
}
main func() i32 {
return derived
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 3)
testing.expect(t, types.equal(hir_module.globals[0].type, types.I32))
testing.expect(t, types.equal(hir_module.globals[1].type, types.I8))
}
@(test)
zero_runtime_calls_fold_with_runtime_fallback :: proc(t: ^testing.T) {
text := `runtime_value i32 = 41
folded func() i32 { return 42 }
by_value func($N usize) usize { return N }
backed func() []i32 {
values [2]mut i32 = [3, 4]
return values[..]
}
fallback func() i32 { return runtime_value }
undefined_result func() i32 {
value i32 = undefined
return value
}
main func() i32 {
if folded() != 42 { return 1 }
if by_value(7) != 7 { return 2 }
view :: backed()
if view[0] != 3 or view[1] != 4 { return 3 }
_ = undefined_result()
return fallback()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
folded_found := false
by_value_found := false
backed_found := false
fallback_found := false
undefined_found := false
for function in hir_module.functions {
name := symbol.resolve(&symbols, function.name)
folded_found = folded_found || name == "folded"
by_value_found = by_value_found || name == "by_value"
backed_found = backed_found || name == "backed"
fallback_found = fallback_found || name == "fallback"
undefined_found = undefined_found || name == "undefined_result"
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, !folded_found)
testing.expect(t, !by_value_found)
testing.expect(t, !backed_found)
testing.expect(t, fallback_found)
testing.expect(t, undefined_found)
}
@(test)
zero_runtime_fold_tracks_mutated_locals_in_dependent_array_types :: proc(t: ^testing.T) {
text := `FoldedMap func($V type) type {
return struct {
keys [][]u8
values []V
indexes []u32
}
}
Pair func($V type) type { return struct { []u8, V } }
build_map func($V type, $N usize, $entries [N]Pair(V)) FoldedMap(V) {
keys [N]mut []u8 = undefined
values [N]mut V = undefined
for entries |entry, i| {
keys[i] = entry.0
values[i] = entry.1
}
for 1..N |i| {
key :: keys[i]
value :: values[i]
j usize = i
while j > 0 and keys[j - 1].len > key.len : j -= 1 {
keys[j] = keys[j - 1]
values[j] = values[j - 1]
}
keys[j] = key
values[j] = value
}
max_len usize :: keys[N - 1].len
indexes [max_len + 1]mut u32 = undefined
for 0..=max_len |length| {
indexes[length] = 0
}
return FoldedMap(V){
keys = keys[..],
values = values[..],
indexes = indexes[..],
}
}
Kind :: enum { short, long }
MAP FoldedMap(Kind) :: build_map([
{"four", .long},
{"a", .short},
])
main func() i32 {
if MAP.keys.len != 2 or MAP.indexes.len != 5 { return 1 }
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
build_map_found := false
for function in hir_module.functions {
build_map_found = build_map_found || symbol.resolve(&symbols, function.name) == "build_map"
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, !build_map_found)
}
@(test)
zero_runtime_fold_preserves_reached_compile_error :: proc(t: ^testing.T) {
text := `fail func() i32 {
compile_error!("folded failure")
return 0
}
main func() i32 { return fail() }
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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, "folded failure")
}
testing.expect(t, found)
}
@(test)
comptime_value_params_specialize_by_value_and_omit_runtime_args :: proc(t: ^testing.T) {
text := `make_array func($N usize) [N]u8 {
data [N]u8 = undefined
return data
}
main func() void {
four [_]u8 :: make_array(4)
eight [_]u8 :: make_array(8)
again [_]u8 :: make_array(4)
literal [_]u8 :: [1, 2, 3, 4]
_ = four
_ = eight
_ = again
_ = literal
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
make_specs := 0
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) == "make_array" {
make_specs += 1
testing.expect_value(t, len(function.params), 0)
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, make_specs, 2)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__make_array__cv4"))
testing.expect(t, strings.contains(llvm_text, "@bro__p0__make_array__cv8"))
}
@(test)
comptime_value_params_diagnose_invalid_uses :: proc(t: ^testing.T) {
text := `make func($N usize) i32 {
return N
}
tiny func($N u8) i32 {
return N
}
good_bool func($T bool) void {}
bad_use func($N usize) void {
N = 1
_ = &N
}
main func() void {
good_bool(true)
x usize = 4
_ = make(x)
_ = make()
_ = make(1, 2)
_ = make(-1)
_ = tiny(300)
bad_use(4)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_runtime_arg := false
found_missing := false
found_extra := false
found_negative := false
found_range := false
found_assignment := false
found_address := false
for diagnostic in diagnostics.items {
message := diagnostic.message
found_runtime_arg = found_runtime_arg || strings.contains(message, "must be a compile-time integer expression")
found_missing = found_missing || strings.contains(message, "cannot infer comptime parameter 'N'")
found_extra = found_extra || strings.contains(message, "has no unique complete argument mapping")
found_negative = found_negative || strings.contains(message, "integer constant -1 does not fit in usize")
found_range = found_range || strings.contains(message, "integer constant 300 does not fit in u8")
found_assignment = found_assignment || strings.contains(message, "cannot assign comptime parameter 'N'")
found_address = found_address || strings.contains(message, "'&' requires an addressable location")
}
testing.expect(t, found_runtime_arg)
testing.expect(t, found_missing)
testing.expect(t, found_extra)
testing.expect(t, found_negative)
testing.expect(t, found_range)
testing.expect(t, found_assignment)
testing.expect(t, found_address)
}
@(test)
inferred_comptime_params_diagnose_ambiguous_calls :: proc(t: ^testing.T) {
text := `Ignored func($T type) type {
return i32
}
Box func($T type) type {
return struct { value T }
}
BoxAlias func($T type) type {
return Box(T)
}
conflict func($T type, left, right T) T { return left }
unknown func($T type) T { value T = undefined; return value }
partial func($T type, $N usize, value T) T { return value }
use_ignored func($T type, value Ignored(T)) i32 { return value }
use_alias func($T type, value BoxAlias(T)) T { return value.value }
mapping_fail func($A usize, value i32, $B usize) void {}
main func() void {
a i32 :: 1
b u32 :: 2
_ = conflict(a, b)
_ = unknown()
_ = partial(i32, a)
_ = use_ignored(a)
box Box(i32) :: Box(i32) { value = 1 }
_ = use_alias(box)
mapping_fail(true, false)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_conflict := false
found_unknown := false
found_partial := false
found_candidate_failures := false
unrecoverable := 0
for diagnostic in diagnostics.items {
message := diagnostic.message
found_conflict = found_conflict || strings.contains(message, "conflicting inference for comptime parameter 'T': i32 and u32")
found_unknown = found_unknown || strings.contains(message, "cannot infer comptime parameter 'T'")
found_partial = found_partial || strings.contains(message, "cannot infer comptime parameter 'N'")
found_candidate_failures = found_candidate_failures ||
strings.contains(message, "candidate 1: cannot infer comptime parameter 'B'") &&
strings.contains(message, "candidate 2: cannot infer comptime parameter 'A'")
if strings.contains(message, "cannot infer comptime parameter 'T'") {
unrecoverable += 1
}
}
testing.expect(t, found_conflict)
testing.expect(t, found_unknown)
testing.expect(t, found_partial)
testing.expect(t, found_candidate_failures)
testing.expect(t, unrecoverable >= 3)
}
@(test)
string_literals_are_contextual_comptime_type_inference_evidence :: proc(t: ^testing.T) {
text := `pair func($T type, expected, actual T) T {
_ = expected
return actual
}
identity func($T type, value T) T {
return value
}
main func() void {
actual []u8 :: "hello"
contextual :: pair("hello", actual)
exact :: identity("hello")
_ = contextual
_ = exact
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
pair_found := false
identity_found := false
for function in hir_module.functions {
name := symbol.resolve(&symbols, function.name)
if name == "pair" {
pair_found = true
testing.expect(t, types.is_slice(function.result, &hir_module.types))
} else if name == "identity" {
identity_found = true
_, array, ok := types.array_pointer(function.result, &hir_module.types)
testing.expect(t, ok && array.child == types.U8 && array.count == 5 &&
array.has_sentinel && array.sentinel == 0)
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, pair_found)
testing.expect(t, identity_found)
}
@(test)
comptime_params_may_be_interleaved_and_are_erased_from_the_abi :: proc(t: ^testing.T) {
text := `valid func($T type, $N usize, value T) [N]T {
result [N]T = undefined
_ = value
return result
}
runtime_first func(value T, $T type, $N usize) T { return value }
split func($T type, value T, $N usize) [N]T {
result [N]T = undefined
_ = value
return result
}
from_result func($T type) T {
value T = undefined
return value
}
choose_mapping func($A usize, value i32, $B usize) [A]u8 {
result [A]u8 = undefined
_ = value
_ = B
return result
}
main func() void {
_ = runtime_first(42, i32, 4)
_ = runtime_first(42, _, 4)
_ = split(i32, 42, 4)
_ = split(42, 4)
value i32 = from_result()
chosen [1]u8 :: choose_mapping(7, 2)
_ = value
_ = chosen
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
for function in hir_module.functions {
name := symbol.resolve(&symbols, function.name)
if name == "runtime_first" || name == "split" {
testing.expect_value(t, len(function.params), 1)
} else if name == "from_result" {
testing.expect_value(t, len(function.params), 0)
} else if name == "choose_mapping" {
testing.expect_value(t, len(function.params), 1)
}
}
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
comptime_type_params_specialize_by_type_and_omit_runtime_args :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
}
id func($T type, value T) T {
return value
}
zero func($T type) T {
value T = undefined
return value
}
buffer func($T type, $N usize, _ T) [N]T {
data [N]T = undefined
return data
}
main func() void {
a i32 :: 42
b u8 :: 7
p Point :: Point { x = 9 }
_ = id(i32, a)
_ = id(u8, b)
_ = id(Point, p)
_ = zero(i32)
_ = buffer(u8, 4, b)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
id_specs := 0
zero_specs := 0
buffer_specs := 0
for function in hir_module.functions {
name := symbol.resolve(&symbols, function.name)
if name == "id" {
id_specs += 1
testing.expect_value(t, len(function.params), 1)
} else if name == "zero" {
zero_specs += 1
testing.expect_value(t, len(function.params), 0)
} else if name == "buffer" {
buffer_specs += 1
testing.expect_value(t, len(function.params), 1)
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, id_specs, 3)
testing.expect_value(t, zero_specs, 1)
testing.expect_value(t, buffer_specs, 1)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__id__i32__cti32"))
testing.expect(t, strings.contains(llvm_text, "@bro__p0__zero__cti32"))
testing.expect(t, strings.contains(llvm_text, "@bro__p0__buffer__u8__ctu8__cv4"))
}
@(test)
comptime_type_params_diagnose_invalid_uses :: proc(t: ^testing.T) {
text := `id func($T type, value T) T {
return value
}
bad_c c_func($T type) void
bad_value func($T type) void {
_ = T
}
bad_assign func($T type) void {
T = 1
}
main func() void {
x i32 = 1
_ = id(x, x)
bad_value(i32)
bad_assign(i32)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_arg := false
found_c_func := false
found_value := false
found_assign := false
for diagnostic in diagnostics.items {
message := diagnostic.message
found_arg = found_arg || strings.contains(message, "argument for comptime type parameter 'T' must be a type")
found_c_func = found_c_func || strings.contains(message, "comptime parameters require 'func', not 'c_func'")
found_value = found_value || strings.contains(message, "type parameter 'T' is not a runtime value")
found_assign = found_assign || strings.contains(message, "cannot assign comptime parameter 'T'")
}
testing.expect(t, found_arg)
testing.expect(t, found_c_func)
testing.expect(t, found_value)
testing.expect(t, found_assign)
}
@(test)
comptime_expression_forces_integer_evaluation :: proc(t: ^testing.T) {
text := `sum func(a, b int) int {
return a + b
}
max func(a, b int) int {
if a > b {
return a
}
return b
}
nested func(value int) int {
two :: 2
return sum(value, two)
}
make_array func($N usize) [N]u8 {
data [N]u8 = undefined
return data
}
forced :: $sum(1, 2)
main func() i32 {
value i32 :: $sum(20, 22)
choice i32 :: $max(9, 3)
blocked i32 :: ${
local :: 5
yield sum(local, 6)
}
bytes [_]u8 :: make_array($nested(2))
if forced != 3 {
return 1
}
if value != 42 {
return 2
}
if choice != 9 {
return 3
}
if blocked != 11 {
return 4
}
if bytes.len != 4 {
return 5
}
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i8 3"))
}
@(test)
comptime_expression_diagnoses_runtime_only_and_quota :: proc(t: ^testing.T) {
text := `native c_func() i32
spin func() i32 {
while true {
}
return 0
}
missing func() i32 {
if true {
}
}
GLOBAL :: 1
main func() void {
runtime i32 = 1
_ = $runtime
_ = $native()
_ = ${
callback :: native
yield callback()
}
_ = $&GLOBAL
_ = ${
values [2]mut i32 = [1, 2]
pointer *mut i32 :: (&values).ptr
yield pointer
}
_ = ${
values [2]mut i32 = [1, 2]
yield values[1..].ptr
}
_ = ${
values [2]mut i32 = [1, 2]
yield &values[0]
}
_ = ${
values [2]mut i32 = [1, 2]
view []mut i32 = values[..]
yield view
}
_ = ${
values [2]mut i32 = [1, 2]
view []i32 = values[1..]
yield view
}
_ = $spin()
_ = $missing()
_ = ${
value :: 1
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_runtime := false
runtime_only_count := 0
pointer_errors := 0
slice_errors := 0
found_quota := false
found_missing := false
found_yield := false
for diagnostic in diagnostics.items {
message := diagnostic.message
found_runtime = found_runtime || strings.contains(message, "unresolved comptime value 'runtime'")
runtime_only_count += 1 if strings.contains(message, "runtime-only") else 0
pointer_errors += 1 if strings.contains(message, "only immutable pointers to whole comptime arrays can materialize as runtime memory") else 0
slice_errors += 1 if strings.contains(message, "only immutable full-array comptime slices can materialize as runtime memory") else 0
found_quota = found_quota || strings.contains(message, "comptime evaluation exceeded the step quota")
found_missing = found_missing || strings.contains(message, "did not return a value")
found_yield = found_yield || strings.contains(message, "a value block must end with an explicit 'yield'")
}
testing.expect(t, found_runtime)
testing.expect(t, runtime_only_count >= 2)
testing.expect(t, pointer_errors >= 4)
testing.expect(t, slice_errors >= 2)
testing.expect(t, found_quota)
testing.expect(t, found_missing)
testing.expect(t, found_yield)
}
@(test)
native_function_pointer_type_restrictions_are_diagnosed :: proc(t: ^testing.T) {
text := `main func() void {
callback @func(...) void = undefined
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_variadic := false
for diagnostic in diagnostics.items {
found_variadic =
found_variadic ||
strings.contains(diagnostic.message, "native function pointer types do not support variadic parameters")
}
testing.expect(t, found_variadic)
}
@(test)
unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) {
text := `broken func(value, value i8, nope void) void {}
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_duplicate := false
found_void := false
for diagnostic in diagnostics.items {
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate parameter 'value'")
found_void = found_void || strings.contains(diagnostic.message, "void is only valid as a function result type")
}
testing.expect(t, found_duplicate)
testing.expect(t, found_void)
}
run_command_success :: proc(command: []string) -> bool {
state, stdout, stderr, err := os2.process_exec(
os2.Process_Desc{command=command},
context.allocator,
)
delete(stdout)
delete(stderr)
return err == nil && state.exit_code == 0
}
prepare_native_object :: proc(output: string) -> bool {
local_cache := fmt.aprintf("ZIG_LOCAL_CACHE_DIR=%s-zig-cache", output)
defer delete(local_cache)
global_cache := fmt.aprintf("ZIG_GLOBAL_CACHE_DIR=%s-zig-global-cache", output)
defer delete(global_cache)
return run_command_success([]string{
"/usr/bin/env",
local_cache,
global_cache,
"zig",
"cc",
"-c",
"examples/interop/manual/native.c",
"-o",
output,
})
}
prepare_native_archive :: proc(object, archive: string) -> bool {
return run_command_success([]string{
"/usr/bin/ar",
"-rcs",
archive,
object,
})
}
run_executable :: proc(path: string) -> os2.Process_State {
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{path}},
context.allocator,
)
delete(stdout)
delete(stderr)
return state
}
@(test)
checker_errors_skip_ir_output_and_backend :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-checker-error-stop"
main_path := "/tmp/brolang-test-checker-error-stop/main.bro"
text := `main func() i32 {
return missing
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
// If compilation reaches temporary IR creation, this nonexistent parent changes
// the result to an infrastructure failure instead of the expected source error.
status := compiler_core.compile_package(directory, "/definitely/not/brolang-output")
testing.expect_value(t, status, 1)
}
@(test)
valid_program_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-valid"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/prototype", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unused_local_warnings_return_status_one_but_do_not_trap :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-unused-locals"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/unused_locals", output)
testing.expect_value(t, status, 1)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 7)
}
@(test)
build_command_is_recognized :: proc(t: ^testing.T) {
testing.expect(t, is_build_command("build"))
testing.expect(t, !is_build_command("translate-c"))
testing.expect(t, !is_build_command("--build"))
}
@(test)
version_command_is_recognized :: proc(t: ^testing.T) {
testing.expect(t, is_version_command("version"))
testing.expect(t, !is_version_command("--version"))
testing.expect(t, len(BROLANG_VERSION) > 0)
}
@(test)
build_subcommand_compiles_and_runs :: proc(t: ^testing.T) {
output := "examples/build/hello/build/hello"
defer _ = os2.remove_all("examples/build/hello/build")
status := compiler_core.run_build("examples/build/hello")
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
status = compiler_core.run_build("examples/build/hello")
testing.expect_value(t, status, 0)
testing.expect(t, os.exists(output))
}
@(test)
build_subcommand_links_c_source_via_list_field :: proc(t: ^testing.T) {
output := "examples/build/manual/build/manual_build"
defer _ = os2.remove_all("examples/build/manual/build")
status := compiler_core.run_build("examples/build/manual")
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
new_project_creates_layout_and_builds :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-new-project"
output := "/tmp/brolang-test-new-project/build/brolang-test-new-project"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect_value(t, run_new_project(root), 0)
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/source"))
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/std"))
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/ffi"))
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/vendor"))
testing.expect(t, os.exists("/tmp/brolang-test-new-project/build.bro"))
testing.expect(t, os.exists("/tmp/brolang-test-new-project/source/main.bro"))
status := compiler_core.run_build(root)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
init_project_creates_missing_layout_and_preserves_existing_files :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-init-project"
output := "/tmp/brolang-test-init-project/build/brolang-test-init-project"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, os2.make_directory_all(root) == nil)
testing.expect(t, init_project(root))
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/source"))
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/std"))
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/ffi"))
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/vendor"))
status := compiler_core.run_build(root)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
custom_build := "custom build"
custom_main := "custom main"
testing.expect(t, os.write_entire_file("/tmp/brolang-test-init-project/build.bro", transmute([]byte)custom_build))
testing.expect(t, os.write_entire_file("/tmp/brolang-test-init-project/source/main.bro", transmute([]byte)custom_main))
testing.expect(t, init_project(root))
build_data, build_ok := os.read_entire_file("/tmp/brolang-test-init-project/build.bro")
defer delete(build_data)
main_data, main_ok := os.read_entire_file("/tmp/brolang-test-init-project/source/main.bro")
defer delete(main_data)
testing.expect(t, build_ok && string(build_data) == custom_build)
testing.expect(t, main_ok && string(main_data) == custom_main)
}
@(test)
build_root_search_finds_nearest_parent_build_file :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-build-search"
nested := "/tmp/brolang-test-build-search/source/nested"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, os2.make_directory_all(nested) == nil)
build_text := "# root\n"
testing.expect(t, os.write_entire_file("/tmp/brolang-test-build-search/build.bro", transmute([]byte)build_text))
found, ok := compiler_core.find_build_root_from(nested)
defer delete(found)
testing.expect(t, ok)
testing.expect(t, strings.has_suffix(found, "/brolang-test-build-search"))
}
@(test)
build_subcommand_discovered_root_writes_to_root_build_dir :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-build-nested"
nested := "/tmp/brolang-test-build-nested/source/nested"
output := "/tmp/brolang-test-build-nested/build/brolang-test-build-nested"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, init_project(root))
testing.expect(t, os2.make_directory_all(nested) == nil)
found, ok := compiler_core.find_build_root_from(nested)
defer delete(found)
testing.expect(t, ok)
status := compiler_core.run_build(found)
testing.expect_value(t, status, 0)
testing.expect(t, os.exists(output))
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
build_subcommand_rejects_path_like_output_name :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-build-invalid-name"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, init_project(root))
text := `b :: import "@std/build"
config :: b.BuildConfig{
name = "bad/name",
source = "source",
libraries = &[],
lib_paths = &[],
includes = &[],
defines = &[],
links = &[],
}
`
testing.expect(t, os.write_entire_file("/tmp/brolang-test-build-invalid-name/build.bro", transmute([]byte)text))
status := compiler_core.run_build(root)
testing.expect_value(t, status, 2)
testing.expect(t, !os.exists("/tmp/brolang-test-build-invalid-name/build/bad/name"))
}
@(test)
project_root_imports_resolve_under_passed_root :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(
"examples/build/raylib/source",
&sources,
&diagnostics,
&symbols,
context.allocator,
context.allocator,
cimport.Options{},
target.DEFAULT,
"examples/build/raylib",
)
defer ast.destroy_module(&ast_module)
testing.expect(t, loaded)
found_vendor := false
for pkg in ast_module.packages {
found_vendor = found_vendor || strings.has_suffix(pkg.path, "/examples/build/raylib/vendor/raylib")
}
testing.expect(t, found_vendor)
}
@(test)
direct_compile_defaults_project_root_to_input_package_and_allows_override :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-direct-root"
app := "/tmp/brolang-test-direct-root/app"
dep := "/tmp/brolang-test-direct-root/dep"
output := "/tmp/brolang-test-direct-root-out"
_ = os2.remove_all(root)
_ = os.remove(output)
defer _ = os2.remove_all(root)
defer _ = os.remove(output)
testing.expect(t, os2.make_directory_all(app) == nil)
testing.expect(t, os2.make_directory_all(dep) == nil)
main_text := "dep :: import \"@dep\"\nmain func() i32 { return dep.value }\n"
dep_text := "value i32 :: 7\n"
testing.expect(t, os.write_entire_file("/tmp/brolang-test-direct-root/app/main.bro", transmute([]byte)main_text))
testing.expect(t, os.write_entire_file("/tmp/brolang-test-direct-root/dep/dep.bro", transmute([]byte)dep_text))
testing.expect_value(t, compiler_core.compile_package(app, output), 1)
status := compiler_core.compile_package(app, output, nil, target.DEFAULT, cimport.Options{}, root)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 7)
}
@(test)
c_printf_accepts_a_string_literal :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-printf"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/interop/printf", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
sentinel_pointer_views_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-sentinel-pointer"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/sentinel_pointer", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 303)
}
@(test)
milestone_24_regressions_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-milestone-24"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/milestone_24", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
milestone_24_rejects_invalid_forms :: proc(t: ^testing.T) {
Case :: struct {
directory: string,
output: string,
}
cases := [?]Case{
{"examples/programs/index_signed_error", "/tmp/brolang-test-index-signed-error"},
{"examples/programs/index_int_constraint_error", "/tmp/brolang-test-index-int-constraint-error"},
{"examples/programs/scalar_cast_error", "/tmp/brolang-test-scalar-cast-error"},
{"examples/programs/array_const_size_error", "/tmp/brolang-test-array-const-size-error"},
}
for test_case in cases {
status := compiler_core.compile_package(test_case.directory, test_case.output)
testing.expect_value(t, status, 1)
_ = os.remove(test_case.output)
}
}
@(test)
function_literals_lower_as_function_pointer_values :: proc(t: ^testing.T) {
text := `Callbacks :: struct {
call @func(value i32) i32
value i32
}
run func(callbacks Callbacks) i32 {
return callbacks.call(callbacks.value)
}
main func() i32 {
callbacks Callbacks = Callbacks {
call = func(value i32) i32 {
return value + 1
},
value = 41,
}
return run(callbacks) - 42
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
generated_functions := 0
literal_exprs := 0
for function in ast_module.functions {
generated_functions += 1 if function.generated else 0
}
for expr in ast_module.exprs {
literal_exprs += 1 if expr.kind == .Function_Literal else 0
}
indirect_calls := 0
for expr in hir_module.exprs {
if expr.kind == .Call && expr.left != hir.INVALID_EXPR {
indirect_calls += 1
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, generated_functions, 1)
testing.expect_value(t, literal_exprs, 1)
testing.expect(t, len(ir_module.functions) > 0)
testing.expect(t, indirect_calls > 0)
}
@(test)
qualified_value_calls_do_not_imply_package_resolution :: proc(t: ^testing.T) {
text := `Callbacks :: struct {
call @func() void
}
main func() void {
callbacks Callbacks = Callbacks{call = func() void {}}
callbacks.call()
missing.call()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 1)
testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'")
testing.expect(t, !strings.contains(diagnostics.items[0].message, "package"))
}
@(test)
function_literals_do_not_capture_locals :: proc(t: ^testing.T) {
text := `main func() i32 {
offset i32 = 1
callback @func(value i32) i32 = func(value i32) i32 {
return value + offset
}
return callback(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "unknown symbol 'offset'")
}
testing.expect(t, found)
}
@(test)
field_function_pointer_calls_reject_non_callable_fields :: proc(t: ^testing.T) {
text := `Box :: struct {
value i32
}
main func() void {
box Box = Box { value = 1 }
box.value()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "call target is not callable")
}
testing.expect(t, found)
}
@(test)
allocator_contract_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-mem-allocator"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/mem_allocator", output, nil, target.DEFAULT, cimport.Options{}, ".")
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load("examples/programs/mem_allocator", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".")
defer ast.destroy_module(&ast_module)
testing.expect(t, loaded)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
found_c_allocator := false
found_anyopaque_context := false
found_vtable_pointer := false
found_alloc_callback := false
found_realloc_callback := false
found_free_callback := false
for global in hir_module.globals {
if symbol.resolve(&symbols, global.name) != "c_allocator" {
continue
}
found_c_allocator = true
for field in types.fields_for(&hir_module.types, global.type) {
name := symbol.resolve(&symbols, symbol.Id(field.name))
if name == "context" {
optional_item, optional_ok := types.node(&hir_module.types, field.type)
if optional_ok && optional_item.kind == .Optional {
pointer_item, pointer_ok := types.node(&hir_module.types, optional_item.child)
found_anyopaque_context = pointer_ok &&
pointer_item.kind == .Pointer &&
pointer_item.mutable &&
!pointer_item.many &&
pointer_item.child == types.ANYOPAQUE
}
} else if name == "vtable" {
pointer_item, pointer_ok := types.node(&hir_module.types, field.type)
found_vtable_pointer = pointer_ok && pointer_item.kind == .Pointer && !pointer_item.mutable && !pointer_item.many
if found_vtable_pointer {
for callback in types.fields_for(&hir_module.types, pointer_item.child) {
callback_name := symbol.resolve(&symbols, symbol.Id(callback.name))
callback_pointer, _, _, callable := types.function_pointer(callback.type, &hir_module.types)
found_alloc_callback = found_alloc_callback || callback_name == "alloc" && callable && !callback_pointer.many
found_realloc_callback = found_realloc_callback || callback_name == "realloc" && callable && !callback_pointer.many
found_free_callback = found_free_callback || callback_name == "free" && callable && !callback_pointer.many
}
}
}
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, len(ir_module.functions) > 0)
testing.expect(t, found_c_allocator)
testing.expect(t, found_anyopaque_context)
testing.expect(t, found_vtable_pointer)
testing.expect(t, found_alloc_callback)
testing.expect(t, found_realloc_callback)
testing.expect(t, found_free_callback)
}
@(test)
milestone_25_c_allocator_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-c-allocator"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/mem_allocator", output, nil, target.DEFAULT, cimport.Options{}, ".")
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
milestone_25_c_allocator_emits_libc_alloc_declarations :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load("examples/programs/mem_allocator", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".")
defer ast.destroy_module(&ast_module)
testing.expect(t, loaded)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "declare ptr @malloc(i64)"))
testing.expect(t, strings.contains(llvm_text, "declare ptr @realloc(ptr, i64)"))
testing.expect(t, strings.contains(llvm_text, "declare i32 @posix_memalign(ptr, i64, i64)"))
testing.expect(t, strings.contains(llvm_text, "declare void @free(ptr)"))
}
@(test)
control_flow_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-control-flow"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/control_flow", output)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
// if / else if / else, comparisons, logical and/or/not, bool locals, and
// block scoping together produce 42.
testing.expect_value(t, state.exit_code, 42)
// Short-circuit: `noisy()` is never reached, so its output must be absent,
// while the taken or-branch must print.
testing.expect(t, !strings.contains(string(stdout), "rhs-evaluated"))
testing.expect(t, strings.contains(string(stdout), "or-taken"))
}
@(test)
break_and_continue_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-break-continue"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/break_continue", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
// `while` break, range-for `continue`, nested innermost-targeting break, a
// `continue` on the final element of an inclusive `u8` range (no overflow
// trap), and an exitable `while true` together produce 42.
testing.expect_value(t, state.exit_code, 42)
}
@(test)
break_and_continue_misuse_is_diagnosed :: proc(t: ^testing.T) {
// `break`/`continue` outside any loop are rejected, and a non-void function
// that exits a `while true` via `break` without returning is flagged as
// missing a return (the `all_paths_return` refinement).
text := `main func() i32 {
bad_break()
bad_continue()
return missing_return()
}
bad_break func() void {
break
}
bad_continue func() void {
continue
}
missing_return func() i32 {
while true {
break
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
break_outside := false
continue_outside := false
missing := false
for diagnostic in diagnostics.items {
break_outside = break_outside || strings.contains(diagnostic.message, "'break' outside of a loop")
continue_outside = continue_outside || strings.contains(diagnostic.message, "'continue' outside of a loop")
missing = missing || strings.contains(diagnostic.message, "'missing_return' does not return a value")
}
testing.expect(t, break_outside)
testing.expect(t, continue_outside)
testing.expect(t, missing)
}
@(test)
defer_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-defer"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/defer", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
// LIFO, runs on fall-through / break / continue / return (with the return value
// captured before defers run), scoped bare blocks, and `defer { ... }` blocks
// together produce 42.
testing.expect_value(t, state.exit_code, 42)
}
@(test)
defer_misuse_is_diagnosed :: proc(t: ^testing.T) {
// Deferring control flow that would escape the defer is rejected: `defer return`,
// `defer break`, and a `return` inside a `defer { ... }` block.
text := `main func() i32 {
bad_defer_return()
bad_defer_break()
bad_return_in_defer()
bad_errdefer_nonfallible()
_ = bad_try_in_defer() catch 0
_ = bad_try_in_errdefer() catch 0
return 0
}
Failure :: enum { bad }
fail func() i32 ! Failure { return .bad }
bad_defer_return func() void {
defer return
}
bad_defer_break func() void {
for 0..3 |i| {
defer break
_ = i
}
}
bad_return_in_defer func() void {
defer {
return
}
}
bad_errdefer_nonfallible func() void {
errdefer {}
}
bad_try_in_defer func() i32 ! Failure {
defer _ = try fail()
return 1
}
bad_try_in_errdefer func() i32 ! Failure {
errdefer _ = try fail()
return 1
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
defer_return := false
defer_break := false
return_in_defer := false
errdefer_nonfallible := false
try_in_defer := 0
for diagnostic in diagnostics.items {
defer_return = defer_return || strings.contains(diagnostic.message, "cannot defer a 'return' statement")
defer_break = defer_break || strings.contains(diagnostic.message, "cannot defer a 'break' statement")
return_in_defer = return_in_defer || strings.contains(diagnostic.message, "cannot 'return' inside a 'defer'")
errdefer_nonfallible = errdefer_nonfallible || strings.contains(diagnostic.message, "'errdefer' requires an enclosing fallible function")
try_in_defer += 1 if strings.contains(diagnostic.message, "cannot 'try' inside a 'defer'") else 0
}
testing.expect(t, defer_return)
testing.expect(t, defer_break)
testing.expect(t, return_in_defer)
testing.expect(t, errdefer_nonfallible)
testing.expect_value(t, try_in_defer, 2)
}
@(test)
errdefer_capture_syntax_is_diagnosed :: proc(t: ^testing.T) {
text := `Failure :: enum { bad }
bad func() i32 ! Failure {
errdefer || {}
errdefer |first, second| {}
return .bad
}
main func() i32 { return bad() catch 0 }
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
missing := false
multiple := false
for diagnostic in diagnostics.items {
missing = missing || strings.contains(diagnostic.message, "expected an errdefer capture name")
multiple = multiple || strings.contains(diagnostic.message, "'errdefer' accepts exactly one capture")
}
testing.expect(t, missing)
testing.expect(t, multiple)
}
@(test)
yield_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-yield"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/yield", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
// Value blocks, value if-statements (incl. `return` branches and unwrap-`if`),
// `orelse`, value loops, labeled value blocks (`blk: { … yield :blk v }`, incl.
// defer-capture, `{T,null}` → optional, and `null` before a concrete yield that uses a
// block local), outer-loop control (`yield :outer v` / `break :outer`), and labeled
// block statements exited via `break :blk` together produce 42.
testing.expect_value(t, state.exit_code, 42)
}
@(test)
bitwise_example_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-bitwise"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/bitwise", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
value_loop_label_does_not_shadow_own_yield_target :: proc(t: ^testing.T) {
text := `main func() i32 {
idx :: for 0..10 |i| hit: {
if (i == 3) yield :hit i
yield null
}
if idx |found| {
if (found == 3) return 0
return 1
}
return 2
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
native_union_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-unions"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/unions", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
// A native untagged union (`Val :: union { n i32, f f64 }`) constructed via a keyed
// literal, stored into a local, and read back through field access reinterprets the
// carrier and yields 42 (declaration → construct → store → load → field read).
testing.expect_value(t, state.exit_code, 42)
}
@(test)
tagged_union_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-tagged-union"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/tagged_union", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
// Both tagged forms — `union(Animal)` (existing enum tag) and `union(enum)` (synthesized
// tag) — constructed via keyed literals, stored as `{tag, payload}`, with the active
// payload read back at its post-tag offset: 37 + 5 = 42. Non-zero payloads make a wrong
// payload offset (e.g. overlapping the tag) fail the exit code.
testing.expect_value(t, state.exit_code, 42)
}
@(test)
tagged_union_stores_the_discriminant :: proc(t: ^testing.T) {
// The runtime test observes only the payload; this one checks the *tag* is written.
// Native sum tags are global u16 IDs. `Animal` contributes dog/cat/bird (1..3),
// then `Data` contributes dog:i32/bird:i32 (4..5), so `Data{ bird = 99 }`
// writes discriminant `store i16 5` beside the payload.
text := `Animal :: enum {
dog
cat
bird
}
Data :: union(Animal) {
dog i32
bird i32
}
main func() i32 {
x Data = Data{ bird = 99 }
return x.bird
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "{ i16, [2 x i8], i32 }"))
testing.expect(t, strings.contains(llvm_text, "store i16 5,"))
testing.expect(t, strings.contains(llvm_text, "store i32 99,"))
}
@(test)
tagged_union_validation_is_diagnosed :: proc(t: ^testing.T) {
// A `union(T)` tag must be an enum, and every variant of a `union(Enum)` must name a
// member of that enum.
text := `Color :: struct {
r u8
}
Animal :: enum {
dog
cat
}
BadTag :: union(Color) {
dog i32
}
BadVariant :: union(Animal) {
snake i32
}
main func() i32 {
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_tag := false
found_variant := false
for diagnostic in diagnostics.items {
found_tag = found_tag || strings.contains(diagnostic.message, "tagged union's tag must be an enum")
found_variant = found_variant || strings.contains(diagnostic.message, "'snake' is not a member of the tag enum")
}
testing.expect(t, found_tag)
testing.expect(t, found_variant)
}
@(test)
sum_composition_merges_widens_and_rejects_conflicts :: proc(t: ^testing.T) {
text := `A :: enum {
same
left
}
B :: enum {
same
right
}
Both :: alias A | B
UA :: union(enum) {
item i32
}
UB :: union(enum) {
empty void
}
UBoth :: alias UA | UB
pick func(value Both) i32 {
match value {
.same: return 1
.left: return 2
.right: return 3
}
}
payload func(value UBoth) i32 {
match value {
.item |n|: return n
.empty: return 5
}
}
main func() i32 {
a A = .left
b B = .right
u UA = UA{ item = 4 }
v UB = .empty
return pick(.same) + pick(a) + pick(b) + payload(u) + payload(v)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64"))
conflict := `A :: union(enum) {
dup i32
}
B :: union(enum) {
dup i64
}
Bad :: alias A | B
main func() void {}
`
conflict_source := source.Source{path="conflict.bro", text=conflict}
conflict_diagnostics := source.init_diagnostics(&conflict_source)
defer source.destroy_diagnostics(&conflict_diagnostics)
conflict_symbols := symbol.init_table()
defer symbol.destroy_table(&conflict_symbols)
conflict_stream := lexer.lex(&conflict_source, &conflict_diagnostics, &conflict_symbols)
defer delete(conflict_stream.items)
conflict_module := parser.parse(&conflict_stream, &conflict_source, &conflict_diagnostics)
defer ast.destroy_module(&conflict_module)
found_conflict := false
for diagnostic in conflict_diagnostics.items {
found_conflict = found_conflict || strings.contains(diagnostic.message, "same variant name")
}
testing.expect(t, found_conflict)
backed := `A :: enum(u8) {
a = 1
}
B :: enum {
b
}
Bad :: alias A | B
main func() void {}
`
backed_source := source.Source{path="backed.bro", text=backed}
backed_diagnostics := source.init_diagnostics(&backed_source)
defer source.destroy_diagnostics(&backed_diagnostics)
backed_symbols := symbol.init_table()
defer symbol.destroy_table(&backed_symbols)
backed_stream := lexer.lex(&backed_source, &backed_diagnostics, &backed_symbols)
defer delete(backed_stream.items)
backed_module := parser.parse(&backed_stream, &backed_source, &backed_diagnostics)
defer ast.destroy_module(&backed_module)
found_backed := false
for diagnostic in backed_diagnostics.items {
found_backed = found_backed || strings.contains(diagnostic.message, "native unbacked")
}
testing.expect(t, found_backed)
}
@(test)
yield_inference_visits_yielded_calls :: proc(t: ^testing.T) {
text := `identity func(value int) int {
return value
}
main func() i32 {
value :: {
yield identity(41)
}
return value - 41
}
`
source_file := source.Source{path="yield_call.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, len(hir_module.functions) > 1)
}
@(test)
return_match_inference_visits_arm_calls :: proc(t: ^testing.T) {
text := `Kind :: enum {
a
b
}
identity func(value i32) i32 {
return value
}
choose func(kind Kind) i32 {
return match kind {
.a: identity(41)
.b: identity(42)
}
}
main func() i32 {
return choose(.a) - 41
}
`
source_file := source.Source{path="return_match_call.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, len(hir_module.functions) > 2)
}
@(test)
fallible_ergonomics_rejects_bad_try_and_catch_blocks :: proc(t: ^testing.T) {
text := `A :: enum {
a
}
B :: enum {
b
}
fa func() i32 ! A {
return .a
}
bad_error func() i32 ! B {
x :: try fa()
return x
}
bad_catch func() i32 {
return fa() catch |e| {
_ = e
}
}
bad_nested_match func() i32 {
return fa() catch |e| {
match e {
.a: yield 3
}
}
}
main func() i32 {
_ = bad_error() catch 0
return bad_catch() + bad_nested_match()
}
`
source_file := source.Source{path="fallible_ergonomics.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_error := false
found_yield := false
found_misplaced_yield := false
for diagnostic in diagnostics.items {
found_error = found_error || strings.contains(diagnostic.message, "'try' error channel cannot be widened")
found_yield = found_yield || strings.contains(diagnostic.message, "a value block must end with an explicit 'yield'")
found_misplaced_yield = found_misplaced_yield || strings.contains(diagnostic.message, "'yield' is only valid as the final statement")
}
testing.expect(t, found_error)
testing.expect(t, found_yield)
testing.expect(t, found_misplaced_yield)
local_success_text := `A :: enum {
a
}
B :: enum {
b
}
Both :: alias A | B
fs func() i64 ! A {
return 1
}
use_success func() void ! Both {
x :: try fs()
_ = x
}
main func() i32 {
use_success() catch |_| { return 1 }
return 0
}
`
directory := "/tmp/brolang-test-try-local-success"
main_path := "/tmp/brolang-test-try-local-success/main.bro"
output := "/tmp/brolang-test-try-local-success-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)local_success_text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
terminating_catch_block_compiles_and_runs :: proc(t: ^testing.T) {
text := `Failure :: enum {
bad
}
may_fail func(fail bool) i32 ! Failure {
if (fail) return .bad
return 1
}
recover func(fail bool) i32 {
value :: may_fail(fail) catch |_| {
return 40
}
return value + 1
}
main func() i32 {
return recover(false) + recover(true) - 42
}
`
directory := "/tmp/brolang-test-terminating-catch"
main_path := "/tmp/brolang-test-terminating-catch/main.bro"
output := "/tmp/brolang-test-terminating-catch-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
catch_fallback_value_sources_compile_and_run :: proc(t: ^testing.T) {
text := `Failure :: enum {
bad
worse
}
value func(fail bool, worse bool) i32 ! Failure {
if fail {
if worse { return .worse }
return .bad
}
return 10
}
void_value func(fail bool) void ! Failure {
if fail { return .bad }
}
capture_score func(err Failure) i32 {
return match err {
.bad: 1
.worse: 2
}
}
consume func(err Failure) void { _ = err }
comptime_recovery func() i32 {
return value(true, false) catch |err| capture_score(err)
}
main func() i32 {
a :: value(true, false) catch 3
b :: value(true, false) catch |err| capture_score(err)
c :: value(true, true) catch |err| match err {
.bad: 4
.worse: 5
}
d :: value(true, false) catch if true {
yield 6
} else {
yield 7
}
e :: value(true, false) catch {
yield 8
}
f :: value(true, false) catch |err| {
_ = err
yield 9
}
g :: value(false, false) catch unreachable
h :: value(false, false) catch |_| unreachable
void_value(true) catch |err| consume(err)
void_value(false) catch {}
ct i32 :: $comptime_recovery()
return a + b + c + d + e + f + g + h + ct - 53
}
`
directory := "/tmp/brolang-test-catch-value-sources"
main_path := "/tmp/brolang-test-catch-value-sources/main.bro"
output := "/tmp/brolang-test-catch-value-sources-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
fallible_void_fallthrough_compiles_at_runtime_and_comptime :: proc(t: ^testing.T) {
text := `Failure :: enum {
bad
}
succeed func() void ! Failure {}
fail func() void ! Failure { return .bad }
maybe func(should_fail bool) void ! Failure {
if should_fail { return .bad }
}
recover func(exit bool) i32 {
fail() catch |_| {
if exit { return 7 }
}
return 0
}
comptime_scenario func() i32 {
succeed() catch |_| {}
fail() catch |_| {}
return 0
}
main func() i32 {
comptime_result i32 :: $comptime_scenario()
handled bool = false
fail() catch |_| { handled = true }
if !handled { return 3 }
succeed() catch |_| { return 1 }
maybe(false) catch |_| { return 2 }
return comptime_result + recover(false) + recover(true) - 7
}
`
directory := "/tmp/brolang-test-fallible-void-fallthrough"
main_path := "/tmp/brolang-test-fallible-void-fallthrough/main.bro"
output := "/tmp/brolang-test-fallible-void-fallthrough-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
matched_error_residuals_return_at_runtime_and_comptime :: proc(t: ^testing.T) {
text := `KeyError :: enum { key_exists }
AllocError :: enum { out_of_memory }
AError :: enum { a }
BError :: enum { b }
CError :: enum { c }
DetailError :: union(enum) { out_of_memory i32 }
Code :: alias i32
key_or_alloc func(code i32) void ! (KeyError | AllocError) {
if code == 1 { return .key_exists }
if code == 2 { return .out_of_memory }
}
abc func(code i32) void ! (AError | BError | CError) {
if code == 1 { return .a }
if code == 2 { return .b }
if code == 3 { return .c }
}
detail func() void ! (KeyError | DetailError) { return .out_of_memory{41} }
via_else func() i32 ! AllocError {
key_or_alloc(2) catch |err| {
match err {
.key_exists: unreachable
else: return err
}
}
return 0
}
via_group func() Code ! (AError | BError) {
abc(2) catch |err| {
match err {
.a, .b: return err
.c: unreachable
}
}
return 0
}
via_nested func() i32 ! AError {
abc(1) catch |err| {
match err {
.a, .b: match err {
.a: return err
else: unreachable
}
.c: unreachable
}
}
return 0
}
via_expand func() i32 ! AError {
abc(1) catch |err| {
match err {
inline |tag|: match tag {
.a: return err
else: unreachable
}
}
}
return 0
}
via_payload func() i32 ! DetailError {
detail() catch |err| {
match err {
.key_exists: unreachable
else: return err
}
}
return 0
}
ct_project func() i32 ! AError {
abc(1) catch |err| {
match err {
.a: return err
else: unreachable
}
}
return 0
}
ct_recover func() i32 {
return ct_project() catch |_| 5
}
main func() i32 {
a :: via_else() catch |_| 1
b :: via_group() catch |err| match err {
.a: 1
.b: 2
}
c :: via_nested() catch |_| 3
d :: via_expand() catch |_| 4
e :: via_payload() catch |err| match err { .out_of_memory |code|: code }
f i32 :: $ct_recover()
return a + b + c + d + e + f - 56
}
`
directory := "/tmp/brolang-test-matched-error-residuals"
main_path := "/tmp/brolang-test-matched-error-residuals/main.bro"
output := "/tmp/brolang-test-matched-error-residuals-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unproven_error_returns_are_rejected_against_both_channels :: proc(t: ^testing.T) {
text := `BadError :: enum { bad }
GoodError :: enum { good }
Combined :: alias (BadError | GoodError)
source func() void ! Combined { return .good }
unrefined func() usize ! GoodError {
source() catch |err| { return err }
return 0
}
incompatible func() usize ! GoodError {
source() catch |err| {
match err {
.bad: return err
else: unreachable
}
}
return 0
}
aliased func() usize ! GoodError {
source() catch |err| {
match err {
.bad: unreachable
else: {
copy :: err
return copy
}
}
}
return 0
}
mutable_subject func() usize ! GoodError {
source() catch |err| {
value Combined = err
match value {
.bad: unreachable
else: return value
}
}
return 0
}
main func() void {
_ = unrefined() catch 0
_ = incompatible() catch 0
_ = aliased() catch 0
_ = mutable_subject() catch 0
}
`
directory := "/tmp/brolang-test-unproven-error-returns"
main_path := "/tmp/brolang-test-unproven-error-returns/main.bro"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(directory, &sources, &diagnostics, &symbols)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
return_errors := 0
for _, diagnostic_index in diagnostics.items {
message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index))
if strings.contains(message, "cannot return") &&
strings.contains(message, "as success type usize or error type GoodError") {
return_errors += 1
}
delete(message)
}
testing.expect_value(t, return_errors, 4)
}
@(test)
conversion_diagnostics_render_source_types :: proc(t: ^testing.T) {
text := `Allocator :: struct {
marker i32
}
take func(allocator Allocator, memory []mut u8) void {}
main func() void {
allocator Allocator = Allocator { marker = 0 }
data [1]mut u8 = [0]
take(data[..], allocator)
}
`
source_file := source.Source{path="type_labels.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
slice_to_allocator := false
allocator_to_slice := false
internal_type_id := false
for diagnostic in diagnostics.items {
slice_to_allocator = slice_to_allocator ||
strings.contains(diagnostic.message, "cannot implicitly convert []mut u8 to Allocator")
allocator_to_slice = allocator_to_slice ||
strings.contains(diagnostic.message, "cannot implicitly convert Allocator to []mut u8")
internal_type_id = internal_type_id || strings.contains(diagnostic.message, "<type ")
}
testing.expect(t, slice_to_allocator)
testing.expect(t, allocator_to_slice)
testing.expect(t, !internal_type_id)
}
@(test)
contextual_payload_variants_compile :: proc(t: ^testing.T) {
text := `DetailError :: union(enum) {
code i32
empty void
}
make_payload func() i32 {
return 7
}
accept func(value DetailError) i32 {
match value {
.code |n|: return n
.empty: return 0
}
}
make_code func(value i32) DetailError {
return .code{value}
}
with_detail func(value i32) i32 ! DetailError {
if (value == 0) return .code{5}
if (value == 1) return .empty
return value
}
main func() i32 {
e DetailError = .code{3}
recovered :: with_detail(0) catch |err| {
result i32 :: match err {
.code |n|: n
.empty: 9
}
yield result
}
return accept(e) + accept(.code{4}) + accept(make_code(5)) + accept(.code{make_payload()}) + recovered - 24
}
`
source_file := source.Source{path="contextual_payload.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64"))
}
@(test)
anonymous_struct_payloads_and_keyed_payload_sugar_compile :: proc(t: ^testing.T) {
text := `PayloadError :: union(enum) {
not_found struct {
path i32
line i32
}
wrapped struct {
path i32
line i32
}
scalar i32
empty void
}
OtherError :: union(enum) {
not_found struct {
path i32
line i32
}
other void
}
BothError :: alias (PayloadError | OtherError)
grouped_error func() void ! (PayloadError | OtherError) {
return .other
}
accept func(value PayloadError) i32 {
match value {
.not_found |info|: return info.path + info.line
.wrapped |info|: return info.path * 10 + info.line
.scalar |n|: return n
.empty: return 0
}
}
with_payload func(value i32) i32 ! PayloadError {
if (value == 0) return .not_found{path = 4, line = 5}
if (value == 1) return .scalar{7}
return value
}
inline_payload func(value i32) i32 ! union(enum) {
inline_bad struct {
code i32
line i32
}
} {
if (value == 0) return .inline_bad{code = 6, line = 7}
return value
}
main func() i32 {
e PayloadError = .not_found{path = 1, line = 2}
a :: accept(e)
b :: accept(.wrapped{line = 4, path = 3})
c :: with_payload(0) catch |err| {
result i32 :: match err {
.not_found |info|: info.path + info.line
.wrapped |info|: info.path + info.line
.scalar |n|: n
.empty: 0
}
yield result
}
d :: inline_payload(0) catch |err| {
result i32 :: match err {
.inline_bad |info|: info.code + info.line
}
yield result
}
return a + b + c + d - 59
}
`
source_file := source.Source{path="anonymous_payloads.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, len(llvm_text) > 0)
}
@(test)
inline_fallible_error_types_compile :: proc(t: ^testing.T) {
text := `inline_enum func(value i32) i32 ! enum {
inline_bad
inline_worse
} {
if (value == 0) return .inline_bad
if (value < 0) return .inline_worse
return value
}
inline_union func(value i32) i32 ! union(enum) {
inline_code i32
inline_empty void
} {
if (value == 0) return .inline_code{6}
if (value == 1) return .inline_empty
return value
}
main func() i32 {
a :: inline_enum(0) catch |e| {
result i32 :: if (e == .inline_bad) {
yield 10
} else {
yield 11
}
yield result
}
b :: inline_union(0) catch |e| {
result i32 :: match e {
.inline_code |n|: n
.inline_empty: 12
}
yield result
}
return a + b - 16
}
`
source_file := source.Source{path="expand_errors.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, len(hir_module.functions) > 1)
testing.expect(t, len(llvm_text) > 0)
}
@(test)
contextual_payload_variants_reject_bad_forms :: proc(t: ^testing.T) {
text := `DetailError :: union(enum) {
code i32
empty void
}
missing_payload func() DetailError {
return .code
}
void_payload func() DetailError {
return .empty{1}
}
empty_payload func() DetailError {
return .code{}
}
multi_payload func() DetailError {
return .code{1, 2}
}
unknown_payload func() DetailError {
return .missing{1}
}
no_context func() i32 {
_ = .code{1}
return 0
}
main func() i32 {
_ = missing_payload()
_ = void_payload()
_ = empty_payload()
_ = multi_payload()
_ = unknown_payload()
return no_context()
}
`
source_file := source.Source{path="bad_contextual_payload.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_missing := false
found_void := false
payload_arity_errors := 0
found_unknown := false
found_no_context := false
for diagnostic in diagnostics.items {
found_missing = found_missing || strings.contains(diagnostic.message, "needs a payload")
found_void = found_void || strings.contains(diagnostic.message, "void variant 'empty' takes no value")
payload_arity_errors += 1 if strings.contains(diagnostic.message, "requires exactly one expression") else 0
found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown variant '.missing'")
found_no_context = found_no_context || strings.contains(diagnostic.message, "requires a tagged-union context")
}
testing.expect(t, found_missing)
testing.expect(t, found_void)
testing.expect(t, payload_arity_errors >= 2)
testing.expect(t, found_unknown)
testing.expect(t, found_no_context)
}
@(test)
anonymous_struct_payloads_reject_bad_forms :: proc(t: ^testing.T) {
text := `Bad :: union(enum) {
payload struct {
x i32
y i32
}
scalar i32
}
unknown func() Bad {
return .payload{x = 1, z = 2}
}
duplicate func() Bad {
return .payload{x = 1, x = 2, y = 3}
}
missing func() Bad {
return .payload{x = 1}
}
scalar_keyed func() Bad {
return .scalar{value = 1}
}
A :: union(enum) {
dup struct {
x i32
}
}
B :: union(enum) {
dup struct {
x i64
}
}
Conflict :: alias A | B
main func() i32 {
_ = unknown()
_ = duplicate()
_ = missing()
_ = scalar_keyed()
return 0
}
`
source_file := source.Source{path="bad_anonymous_payloads.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_unknown := false
found_duplicate := false
found_missing := false
found_keyed_non_struct := false
found_conflict := false
for diagnostic in diagnostics.items {
found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown struct field 'z'")
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate initializer for struct field 'x'")
found_missing = found_missing || strings.contains(diagnostic.message, "missing initializer for struct field 'y'")
found_keyed_non_struct = found_keyed_non_struct || strings.contains(diagnostic.message, "keyed contextual payload requires a struct payload")
found_conflict = found_conflict || strings.contains(diagnostic.message, "same variant name")
}
testing.expect(t, found_unknown)
testing.expect(t, found_duplicate)
testing.expect(t, found_missing)
testing.expect(t, found_keyed_non_struct)
testing.expect(t, found_conflict)
}
@(test)
errors_example_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-errors"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/errors", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
match_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-match"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/match", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
// Exercises every match form — tagged-union statement match with payload capture,
// tagged-union value match (implicit + explicit yield), enum statement/value match,
// and integer match with `else` — all summed to a self-checking 0 on success.
testing.expect_value(t, state.exit_code, 0)
}
@(test)
match_dispatches_on_the_tag :: proc(t: ^testing.T) {
// A tagged-union match desugars to: read the discriminant once (a load of the tag
// enum at the union's offset 0), then compare it against each variant's tag value.
// Data's runtime tag is the hidden global u16 variant ID.
text := `Animal :: enum {
dog
cat
bird
}
Data :: union(Animal) {
dog i32
bird i32
}
main func() i32 {
d Data = Data{ bird = 7 }
out i32 = 0
match d {
.dog |v|: out = v
.bird |v|: out = v + 1
}
return out
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "load i16"))
testing.expect(t, strings.contains(llvm_text, "icmp eq i16"))
}
@(test)
match_misuse_is_diagnosed :: proc(t: ^testing.T) {
// Five rejected matches: (1) a non-exhaustive enum match with no `else`, (2) a scalar
// match missing its mandatory `else`, (3) a payload capture on a non-union arm, (4) a
// redundant `else` on an already-exhaustive match, and (5) an unknown variant.
text := `Animal :: enum {
dog
cat
bird
}
Data :: union(Animal) {
dog i32
bird i32
}
not_exhaustive func(a Animal) i32 {
match a {
.dog: { return 1 }
.cat: { return 2 }
}
return 0
}
missing_else func(n i32) i32 {
match n {
0: { return 1 }
1: { return 2 }
}
return 0
}
bad_capture func(a Animal) i32 {
match a {
.dog |v|: { return 1 }
.cat: { return 2 }
.bird: { return 3 }
}
return 0
}
redundant_else func(a Animal) i32 {
match a {
.dog: { return 1 }
.cat: { return 2 }
.bird: { return 3 }
else: { return 4 }
}
return 0
}
unknown_variant func(d Data) i32 {
match d {
.dog: { return 1 }
.snake: { return 2 }
else: { return 3 }
}
return 0
}
main func() i32 {
# Functions are specialized on use, so call each so its body is type-checked.
d Data = Data{ dog = 0 }
return not_exhaustive(.dog) + missing_else(0) + bad_capture(.dog) +
redundant_else(.dog) + unknown_variant(d)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_exhaustive := false
found_missing_else := false
found_capture := false
found_redundant := false
found_unknown := false
for diagnostic in diagnostics.items {
found_exhaustive = found_exhaustive || strings.contains(diagnostic.message, "is not exhaustive")
found_missing_else = found_missing_else || strings.contains(diagnostic.message, "requires an 'else' arm")
found_capture = found_capture || strings.contains(diagnostic.message, "only tagged-union variants can capture")
found_redundant = found_redundant || strings.contains(diagnostic.message, "redundant 'else'")
found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown variant '.snake'")
}
testing.expect(t, found_exhaustive)
testing.expect(t, found_missing_else)
testing.expect(t, found_capture)
testing.expect(t, found_redundant)
testing.expect(t, found_unknown)
}
@(test)
match_range_arm_emits_bounds :: proc(t: ^testing.T) {
// A scalar range arm `lo..hi:` desugars to `key >= lo and key < hi` (inclusive uses
// `<=`), emitted as signed integer comparisons for an i32 subject.
text := `main func() i32 {
n i32 = 5
out i32 = 0
match n {
0..10: out = 1
10..=20: out = 2
else: out = 3
}
return out
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "icmp sge i32")) // key >= lo
testing.expect(t, strings.contains(llvm_text, "icmp slt i32")) // key < hi (exclusive)
testing.expect(t, strings.contains(llvm_text, "icmp sle i32")) // key <= hi (inclusive)
}
@(test)
match_extended_misuse_is_diagnosed :: proc(t: ^testing.T) {
// Five rejected forms from milestone 22.5: (1) a capture on a void variant, (2) a value
// given to a void variant in construction, (3) a bare key on a non-void field, (4) a
// range pattern on an enum subject, and (5) a multi-pattern capture whose variants have
// different payload types. (Pointer capture on an rvalue subject is also rejected, but
// match-on-call-result is a separate pre-existing gap so it isn't exercised here.)
text := `Animal :: enum {
dog
cat
bird
}
Point :: struct {
x i32
y i32
}
Box :: union(enum) {
point Point
count i32
empty void
}
void_capture func(b Box) i32 {
match b {
.point |p|: { return p.x }
.count |c|: { return c }
.empty |x|: { return 0 }
}
return 0
}
void_value func() i32 {
b Box = Box{ empty = 5 }
return 0
}
bare_on_nonvoid func() i32 {
b Box = Box{ count }
return 0
}
range_on_enum func(a Animal) i32 {
match a {
0..2: { return 1 }
else: { return 0 }
}
return 0
}
incompatible_capture func(b Box) i32 {
match b {
.point, .count |v|: { return 0 }
.empty: { return 0 }
}
return 0
}
main func() i32 {
b Box = Box{ count = 1 }
return void_capture(b) + void_value() + bare_on_nonvoid() + range_on_enum(.dog) +
incompatible_capture(b)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_void_capture := false
found_void_value := false
found_bare := false
found_range_enum := false
found_incompatible := false
for diagnostic in diagnostics.items {
found_void_capture = found_void_capture || strings.contains(diagnostic.message, "void payload")
found_void_value = found_void_value || strings.contains(diagnostic.message, "void variant 'empty' takes no value")
found_bare = found_bare || strings.contains(diagnostic.message, "field 'count' requires a value")
found_range_enum = found_range_enum || strings.contains(diagnostic.message, "range patterns only apply to scalar")
found_incompatible = found_incompatible || strings.contains(diagnostic.message, "capture group with incompatible types")
}
testing.expect(t, found_void_capture)
testing.expect(t, found_void_value)
testing.expect(t, found_bare)
testing.expect(t, found_range_enum)
testing.expect(t, found_incompatible)
}
@(test)
match_call_subject_and_contextual_void_compile :: proc(t: ^testing.T) {
// Milestone 22.6: (1) a call expression directly as the match subject (`match get()`)
// now specializes — previously "could not resolve specialization of 'get'" — because the
// inference pass visits the match subject; (2) a void variant constructed contextually
// (`e Box = .empty`) coerces the bare enum literal to the union. Both compile clean to IR.
text := `Animal :: enum {
dog
cat
bird
}
Box :: union(enum) {
count i32
empty void
}
get func() Animal {
return .bird
}
main func() i32 {
e Box = .empty
r i32 = 0
match get() {
.dog: r = 1
.cat: r = 2
.bird: r = 3
}
match e {
.count |c|: r = r + c
.empty: r = r + 7
}
return r
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "icmp eq")) // the call subject still dispatches
}
@(test)
match_contextual_payload_variant_is_diagnosed :: proc(t: ^testing.T) {
// Contextual construction is only for void variants; a bare `.point` for a payload
// variant must use `Box{ point = ... }` instead.
text := `Point :: struct {
x i32
y i32
}
Box :: union(enum) {
point Point
empty void
}
bad func() i32 {
e Box = .point
return 0
}
main func() i32 {
return bad()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "needs a payload")
}
testing.expect(t, found)
}
@(test)
yield_misuse_is_diagnosed :: proc(t: ^testing.T) {
// A value block that does not end in `yield`, and a `yield` nested inside an
// `if` within a value block (only the final statement may yield).
text := `main func() i32 {
missing :: {
k :: 5
}
nested :: {
if (true) {
yield 1
}
yield 2
}
return missing + nested
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
missing_yield := false
misplaced_yield := false
for diagnostic in diagnostics.items {
missing_yield = missing_yield || strings.contains(diagnostic.message, "a value block must end with an explicit 'yield'")
misplaced_yield = misplaced_yield || strings.contains(diagnostic.message, "'yield' is only valid as the final statement of a value block")
}
testing.expect(t, missing_yield)
testing.expect(t, misplaced_yield)
}
@(test)
comptime_yield_targets_match_value_source_semantics :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-comptime-yield-targets"
main_path := "/tmp/brolang-test-comptime-yield-targets/main.bro"
output := "/tmp/brolang-test-comptime-yield-targets-output"
valid_text := `selected :: ${
via_if :: if true {
yield 1
} else {
yield 2
}
via_label :: done: {
if true {
yield :done 3
}
yield :done 4
}
yield via_if + via_label
}
main func() i32 { return selected - 4 }
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)valid_text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
invalid_text := `invalid :: ${
if false {
yield 1
}
yield 2
}
main func() void {}
`
source_file := source.Source{path="invalid_comptime_yield.bro", text=invalid_text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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, "'yield' is only valid as the final statement of a value block")
}
testing.expect(t, found)
}
@(test)
yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
// Value if/loop/block misuse: an `if` value without an `else`; a branch that neither
// yields nor exits on every path (20.6); a value loop whose body lacks a trailing
// fall-through `yield`; a `yield :label` with no matching value loop; a labeled value
// block that does not yield on every path; a `break :label` naming no loop; and a
// `continue :label` targeting a block (not a loop).
text := `main func() i32 {
noelse :: if (true) {
yield 1
}
badbranch :: if (true) {
k :: 5
} else {
yield 2
}
noloopyield :: for 0..10 |i| blk: {
if (i == 0) yield :blk i
}
for 0..10 |j| stray: {
yield :stray j
}
noblockyield :: blk: {
if (true) yield :blk 1
k2 :: 5
}
for 0..10 |m| {
break :nope
}
scope: {
continue :scope
}
return noelse + badbranch + noloopyield + noblockyield
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
no_else := false
branch_no_yield := false
loop_no_yield := false
stray_label := false
block_no_yield := false
bad_break_label := false
continue_block := false
for diagnostic in diagnostics.items {
no_else = no_else || strings.contains(diagnostic.message, "an 'if' used as a value must have an 'else'")
branch_no_yield = branch_no_yield || strings.contains(diagnostic.message, "a value branch must end with 'yield' or exit on every path")
loop_no_yield = loop_no_yield || strings.contains(diagnostic.message, "a value loop's body must end with a 'yield'")
stray_label = stray_label || strings.contains(diagnostic.message, "no enclosing value loop or block is labeled 'stray'")
block_no_yield = block_no_yield || strings.contains(diagnostic.message, "a labeled value block must 'yield' on every path")
bad_break_label = bad_break_label || strings.contains(diagnostic.message, "no enclosing loop is labeled 'nope'")
// `continue :scope` targets a labeled block, which is not a loop.
continue_block = continue_block || strings.contains(diagnostic.message, "no enclosing loop is labeled 'scope'")
}
testing.expect(t, no_else)
testing.expect(t, branch_no_yield)
testing.expect(t, loop_no_yield)
testing.expect(t, stray_label)
testing.expect(t, block_no_yield)
testing.expect(t, bad_break_label)
testing.expect(t, continue_block)
}
@(test)
foreign_function_links_from_c_source :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-foreign-source"
defer _ = os.remove(output)
arguments := []linker.Argument{{kind=.Input, value="examples/interop/manual/native.c"}}
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
restricted_c_header_imports_compile_and_link :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-header-import"
defer _ = os.remove(output)
arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}}
c_options := cimport.Options{
include_paths=[]string{"examples/interop/header/include"},
defines=[]string{"BROLANG_FEATURE"},
}
status := compiler_core.compile_package("examples/interop/header/app", output, arguments, target.DEFAULT, c_options)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
by_value_c_records_and_unions_compile_and_link :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-records"
defer _ = os.remove(output)
arguments := []linker.Argument{{kind=.Input, value="examples/interop/records/native.c"}}
c_options := cimport.Options{include_paths=[]string{"examples/interop/records/include"}}
status := compiler_core.compile_package("examples/interop/records/app", output, arguments, target.DEFAULT, c_options)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 1)
}
@(test)
self_referential_c_records_import_and_compile :: proc(t: ^testing.T) {
// `struct Node { struct Node *next; int value; }`: the importer must not recurse
// forever populating the self-referential record, and the checker must accept the
// self-referential `?*mut Node` field as C-layout-compatible.
output := "/tmp/brolang-test-recursive"
defer _ = os.remove(output)
c_options := cimport.Options{include_paths=[]string{"examples/interop/recursive/include"}}
status := compiler_core.compile_package("examples/interop/recursive/app", output, nil, target.DEFAULT, c_options)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unsupported_c_header_members_diagnose_only_when_referenced :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-header-unsupported"
defer _ = os.remove(output)
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
status := compiler_core.compile_package("examples/interop/header_unsupported", output, nil, target.DEFAULT, c_options)
testing.expect_value(t, status, 1)
testing.expect(t, !os.exists(output))
}
@(test)
compatible_c_header_redeclarations_share_one_llvm_declaration :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-header-duplicate"
defer _ = os.remove(output)
arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}}
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
status := compiler_core.compile_package("examples/interop/header_duplicate", output, arguments, target.DEFAULT, c_options)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
interop_foundation_matches_zig_compiled_apple_silicon_c_fixture :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-interop-foundation"
defer _ = os.remove(output)
arguments := []linker.Argument{{kind=.Input, value="examples/interop/foundation/native.c"}}
status := compiler_core.compile_package("examples/interop/foundation", output, arguments)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 1)
}
@(test)
foreign_function_links_from_object :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-foreign-object"
object := "/tmp/brolang-test-foreign-object.o"
defer _ = os.remove(output)
defer _ = os.remove(object)
testing.expect(t, prepare_native_object(object))
arguments := []linker.Argument{{kind=.Input, value=object}}
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
foreign_function_links_from_direct_library :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-foreign-direct-library"
object := "/tmp/brolang-test-foreign-direct-library.o"
archive := "/tmp/brolang-test-foreign-direct-library.a"
defer _ = os.remove(output)
defer _ = os.remove(object)
defer _ = os.remove(archive)
testing.expect(t, prepare_native_object(object))
testing.expect(t, prepare_native_archive(object, archive))
arguments := []linker.Argument{{kind=.Input, value=archive}}
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
foreign_function_links_from_named_library :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-foreign-named-library"
object := "/tmp/brolang-test-foreign-named-library.o"
archive := "/tmp/libbrolang-test-foreign-named.a"
defer _ = os.remove(output)
defer _ = os.remove(object)
defer _ = os.remove(archive)
testing.expect(t, prepare_native_object(object))
testing.expect(t, prepare_native_archive(object, archive))
arguments := []linker.Argument{
{kind=.Library_Path, value="/tmp"},
{kind=.Library, value="brolang-test-foreign-named"},
}
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
one_line_main_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-one-line-main"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/one_line", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 7)
}
@(test)
folded_constant_addition_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-constant-fold"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/constant_fold", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
main_int_is_constrained_to_i32 :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-main-int"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/main_int", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 3)
}
@(test)
main_i32_returns_directly :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-main-i32"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/main_i32", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 4)
}
@(test)
checked_addition_traps_on_overflow :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-overflow"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/overflow", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
checked_distinct_addition_traps_on_backing_overflow :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-distinct-overflow"
main_path := "/tmp/brolang-test-distinct-overflow/main.bro"
output := "/tmp/brolang-test-distinct-overflow-output"
text := `D :: distinct i8
add func(left, right D) D { return left + right }
main func() i32 {
_ = add(D(127), D(1))
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect(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_stops_before_backend :: 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)
testing.expect(t, !os.exists(output))
}
@(test)
constant_beyond_i64_stops_before_backend :: 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)
testing.expect(t, !os.exists(output))
}
@(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_stops_before_backend :: 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)
testing.expect(t, !os.exists(output))
}
@(test)
backend_failure_preserves_existing_output :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-preserved-output"
defer _ = os.remove(output)
previous := "previous artifact"
testing.expect(t, os.write_entire_file(output, transmute([]byte)previous))
testing.expect(t, !backend.compile("/definitely/not/llvm.ll", output))
data, ok := os.read_entire_file(output)
defer delete(data)
testing.expect(t, ok)
testing.expect_value(t, string(data), previous)
}
@(test)
backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T) {
arguments := []linker.Argument{
{kind=.Input, value="native.c"},
{kind=.Library_Path, value="vendor/lib"},
{kind=.Library, value="thing"},
{kind=.Input, value="helper.o"},
}
c_options := cimport.Options{
include_paths=[]string{"vendor/include"},
defines=[]string{"FEATURE=1"},
}
command := backend.build_command("module.ll", "program", arguments, target.DEFAULT, c_options)
defer backend.destroy_command(command)
expected_prefix := []string{
"/usr/bin/env",
"zig",
"cc",
"-target",
"aarch64-macos",
"-Wno-override-module",
"-Wno-unused-command-line-argument",
}
testing.expect(t, len(command) >= len(expected_prefix))
for value, index in expected_prefix {
testing.expect_value(t, command[index], value)
}
tail_index := len(expected_prefix)
if sdk, ok := backend.macos_sdk_root(); ok {
defer delete(sdk)
frameworks := fmt.tprintf("-F%s/System/Library/Frameworks", sdk)
lib_dir := fmt.tprintf("-L%s/usr/lib", sdk)
testing.expect(t, len(command) >= tail_index + 2)
testing.expect_value(t, command[tail_index], frameworks)
testing.expect_value(t, command[tail_index + 1], lib_dir)
tail_index += 2
}
expected_tail := []string{
"module.ll",
"-Ivendor/include",
"-DFEATURE=1",
"native.c",
"-Lvendor/lib",
"-lthing",
"helper.o",
"-o",
"program",
}
testing.expect_value(t, len(command), tail_index + len(expected_tail))
for value, index in expected_tail {
testing.expect_value(t, command[tail_index + index], value)
}
}
Fake_Cimport_State :: struct {
calls: int,
available: bool,
infrastructure: bool,
saw_options: bool,
}
Conflict_Cimport_State :: struct {
calls: int,
}
Symbol_Conflict_Cimport_State :: struct {
calls: int,
}
fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
state := (^Fake_Cimport_State)(user_data)
state.calls += 1
state.saw_options =
len(request.include_paths) == 2 &&
request.include_paths[0] == "first/include" &&
request.include_paths[1] == "second/include" &&
len(request.defines) == 2 &&
request.defines[0] == "FIRST=1" &&
request.defines[1] == "SECOND" &&
request.target == target.DEFAULT
result := cimport.init_result(allocator)
if !state.available {
result.infrastructure = state.infrastructure
result.error_message = fmt.aprintf("fake importer unavailable", allocator=allocator)
return result
}
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
append(&result.functions, cimport.Function{
name=fmt.aprintf("fake_value", allocator=allocator),
result=cimport.Type_Id(0),
variadic=true,
reason=fmt.aprintf("", allocator=allocator),
})
append(&result.variables, cimport.Variable{
name=fmt.aprintf("fake_global", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
append(&result.macros, cimport.Macro_Constant{
name=fmt.aprintf("FAKE_MAGIC", allocator=allocator),
type=cimport.Type_Id(0),
value={kind=.Integer, type=cimport.Type_Id(0), integer=7},
reason=fmt.aprintf("", allocator=allocator),
})
result.available = true
return result
}
conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
state := (^Conflict_Cimport_State)(user_data)
state.calls += 1
result := cimport.init_result(allocator)
kind := cimport.Type_Kind.C_Int
if strings.has_suffix(request.path, "second.h") {
kind = .C_Long
}
append(&result.types, cimport.Type{kind=kind, child=cimport.INVALID_TYPE})
append(&result.variables, cimport.Variable{
name=fmt.aprintf("conflict_global", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
result.available = true
return result
}
symbol_conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
state := (^Symbol_Conflict_Cimport_State)(user_data)
state.calls += 1
result := cimport.init_result(allocator)
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
if strings.has_suffix(request.path, "variable.h") {
append(&result.variables, cimport.Variable{
name=fmt.aprintf("conflict_symbol", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
} else {
append(&result.functions, cimport.Function{
name=fmt.aprintf("conflict_symbol", allocator=allocator),
result=cimport.Type_Id(0),
reason=fmt.aprintf("", allocator=allocator),
})
}
result.available = true
return result
}
main_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
result := cimport.init_result(allocator)
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
append(&result.variables, cimport.Variable{
name=fmt.aprintf("main", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
result.available = true
return result
}
write_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
result := cimport.init_result(allocator)
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
append(&result.variables, cimport.Variable{
name=fmt.aprintf("write", allocator=allocator),
type=cimport.Type_Id(0),
mutable=true,
reason=fmt.aprintf("", allocator=allocator),
})
result.available = true
return result
}
count_substring_occurrences :: proc(text, needle: string) -> int {
if len(needle) == 0 {
return 0
}
count := 0
for index := 0; index + len(needle) <= len(text); index += 1 {
if text[index:index + len(needle)] == needle {
count += 1
}
}
return count
}
find_substring_offset :: proc(text, needle: string) -> int {
if len(needle) == 0 {
return 0
}
for index := 0; index + len(needle) <= len(text); index += 1 {
if text[index:index + len(needle)] == needle {
return index
}
}
return -1
}
find_cimport_variable :: proc(result: ^cimport.Result, name: string) -> (^cimport.Variable, bool) {
for &variable in result.variables {
if variable.name == name {
return &variable, true
}
}
return nil, false
}
count_cimport_variables :: proc(result: ^cimport.Result, name: string) -> int {
count := 0
for variable in result.variables {
if variable.name == name {
count += 1
}
}
return count
}
find_cimport_macro :: proc(result: ^cimport.Result, name: string) -> (^cimport.Macro_Constant, bool) {
for &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)
diagnostic_warnings_format_and_dedupe_by_severity :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="one\n"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
span := source.Span{start=0, end=3}
warning := source.add_warning(&diagnostics, span, "same")
warning_again := source.addf_warning(&diagnostics, span, "%s", "same")
err := source.add(&diagnostics, span, "same")
formatted_warning := source.format(&diagnostics, warning)
defer delete(formatted_warning)
formatted_error := source.format(&diagnostics, err)
defer delete(formatted_error)
testing.expect_value(t, warning, warning_again)
testing.expect(t, warning != err)
testing.expect_value(t, len(diagnostics.items), 2)
testing.expect_value(t, diagnostics.items[warning].severity, source.Severity.Warning)
testing.expect_value(t, diagnostics.items[err].severity, source.Severity.Error)
testing.expect(t, strings.contains(formatted_warning, "warning: same\n --> test.bro:1:1"))
testing.expect(t, strings.contains(formatted_error, "error: same\n --> test.bro:1:1"))
}
@(test)
rich_diagnostics_render_labels_notes_help_and_tabs :: proc(t: ^testing.T) {
store := source.init_store()
defer source.destroy_store(&store)
primary_file := source.add_source(&store, "main.bro", "\tmissing()\nnext()\n")
definition_file := source.add_source(&store, "dep.bro", "value :: 1\n")
diagnostics := source.init_store_diagnostics(&store)
defer source.destroy_diagnostics(&diagnostics)
id := source.add(&diagnostics, source.Span{file=primary_file, start=1, end=8}, "unknown symbol 'missing'")
source.set_primary_label(&diagnostics, id, "unknown symbol")
source.set_primary_label(&diagnostics, id, "unknown symbol")
source.add_secondary_label(
&diagnostics, id,
source.Span{file=definition_file, start=0, end=5},
"related declaration",
)
source.add_note(&diagnostics, id, "names resolve in the current scope")
source.add_note(&diagnostics, id, "names resolve in the current scope")
source.add_help(&diagnostics, id, "declare 'missing' before using it")
formatted := source.format(&diagnostics, id)
defer delete(formatted)
multiline := source.add(
&diagnostics,
source.Span{file=primary_file, start=1, end=17},
"multiline failure",
)
multiline_formatted := source.format(&diagnostics, multiline)
defer delete(multiline_formatted)
unknown := source.add(&diagnostics, source.Span{}, "no location")
unknown_formatted := source.format(&diagnostics, unknown)
defer delete(unknown_formatted)
testing.expect_value(t, len(diagnostics.annotations), 4)
testing.expect(t, strings.contains(formatted, "error: unknown symbol 'missing'"))
testing.expect(t, strings.contains(formatted, "--> main.bro:1:2"))
testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol"))
testing.expect(t, strings.contains(formatted, "::: dep.bro:1:1"))
testing.expect(t, strings.contains(formatted, "----- related declaration"))
testing.expect(t, strings.contains(formatted, "note: names resolve in the current scope"))
testing.expect(t, strings.contains(formatted, "help: declare 'missing' before using it"))
testing.expect(t, strings.contains(multiline_formatted, "1 | missing()"))
testing.expect(t, !strings.contains(multiline_formatted, "next()"))
testing.expect_value(t, unknown_formatted, "main.bro: error: no location")
}
@(test)
poisoned_expressions_preserve_independent_root_diagnostics :: proc(t: ^testing.T) {
text := `bad :: missing_global
sink func(value i32) void { _ = value }
broken func(value int) int { return missing_return + value }
main func() void {
_ = missing_add + 1
_ = try missing_try()
missing_catch() catch |_| {}
sink(missing_arg)
missing_stmt()
missing_target.field = 1
value i32 = 0
value += missing_rhs
_ = broken(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 9)
for diagnostic in diagnostics.items {
testing.expect(t, strings.has_prefix(diagnostic.message, "unknown symbol 'missing_"))
testing.expect(t, !strings.contains(diagnostic.message, "fallible expression"))
testing.expect(t, !strings.contains(diagnostic.message, "must be consumed"))
testing.expect(t, !strings.contains(diagnostic.message, "compatible numeric operands"))
testing.expect(t, !strings.contains(diagnostic.message, "implicitly convert"))
}
}
@(test)
maximum_signed_i64_literal_parses_exactly :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="value :: 9223372036854775807\nmain func() void {}\n"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(9223372036854775807))
}
@(test)
negative_constants_fold_and_accept_signed_i64_minimum :: proc(t: ^testing.T) {
text := `minimum :: -9223372036854775808
grouped i64 :: -(9223372036854775808)
folded :: -(1 + 2)
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, types.equal(hir_module.globals[0].type, types.I64))
testing.expect(t, types.equal(hir_module.globals[1].type, types.I64))
testing.expect(t, types.equal(hir_module.globals[2].type, types.I8))
testing.expect_value(t, hir_module.globals[0].static_value, i64(-9223372036854775807-1))
testing.expect_value(t, hir_module.globals[1].static_value, i64(-9223372036854775807-1))
testing.expect_value(t, hir_module.globals[2].static_value, i64(-3))
}
@(test)
constant_division_by_zero_has_a_precise_diagnostic :: proc(t: ^testing.T) {
text := `value :: divtrunc!(5, 0)
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_division_by_zero := false
found_overflow := false
for diagnostic in diagnostics.items {
found_division_by_zero = found_division_by_zero ||
strings.contains(diagnostic.message, "division builtin denominator is zero")
found_overflow = found_overflow ||
strings.contains(diagnostic.message, "integer constant expression exceeds signed i64 range")
}
testing.expect(t, found_division_by_zero)
testing.expect(t, !found_overflow)
}
@(test)
out_of_range_negative_constants_are_diagnosed :: proc(t: ^testing.T) {
text := `positive :: 9223372036854775808
below_minimum :: -9223372036854775809
double_minimum :: --9223372036854775808
maximum_u64 :: 18446744073709551615
beyond_u64 :: 18446744073709551616
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_expression_range := 0
found_literal_range := 0
found_u64_range := false
for diagnostic in diagnostics.items {
found_expression_range += 1 if strings.contains(diagnostic.message, "exceeds signed i64 range") else 0
found_literal_range += 1 if strings.contains(diagnostic.message, "does not fit in i64") else 0
found_u64_range = found_u64_range || strings.contains(diagnostic.message, "magnitude does not fit in u64")
}
testing.expect_value(t, found_expression_range, 2)
testing.expect_value(t, found_literal_range, 2)
testing.expect(t, found_u64_range)
}
@(test)
runtime_negation_preserves_operand_type_before_result_widening :: proc(t: ^testing.T) {
text := `negate_i8 func(value i8) i8 {
return -value
}
widen_after_negate func(value i8) i16 {
return -value
}
main func() void {
_ = negate_i8(1)
_ = widen_after_negate(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i8"))
found_widen := false
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) != "widen_after_negate" {
continue
}
statement := hir_module.statements[function.body[0]]
widen := hir_module.exprs[statement.expr]
negate := hir_module.exprs[widen.left]
testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen)
testing.expect(t, types.equal(widen.type, types.I16))
testing.expect_value(t, negate.kind, hir.Expr_Kind.Negate)
testing.expect(t, types.equal(negate.type, types.I8))
found_widen = true
}
testing.expect(t, found_widen)
}
@(test)
malformed_hir_references_lower_to_valid_trapped_llvm :: proc(t: ^testing.T) {
hir_module := hir.init_module()
defer hir.destroy_module(&hir_module)
append(&hir_module.exprs, hir.Expr{
kind=.Local,
type=types.I8,
target=hir.local_ref(hir.INVALID_LOCAL),
left=hir.INVALID_EXPR,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append(&hir_module.statements, hir.Stmt{
kind=.Expression,
expr=hir.Expr_Id(0),
local=hir.INVALID_LOCAL,
diagnostic=source.INVALID_DIAGNOSTIC,
})
body := make([]hir.Stmt_Id, 1)
body[0] = hir.Stmt_Id(0)
append(&hir_module.functions, hir.Function{
name=symbol.INVALID,
link_name=strings.clone("main"),
calling_convention=.C,
implementation=.Definition,
linkage=.External,
is_main=true,
result=types.VOID,
body=body,
diagnostic=source.INVALID_DIAGNOSTIC,
})
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
source_file := source.Source{path="test.bro", text=""}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(text)
testing.expect(t, strings.contains(text, "call void @bro.trap"))
testing.expect(t, !strings.contains(text, "%v-1"))
}
@(test)
malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) {
module := ir.init_module()
defer ir.destroy_module(&module)
instructions := make([]ir.Instruction, 11)
instructions[0] = ir.Instruction{op=.Store, type=types.I8, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
instructions[1] = ir.Instruction{op=.Add_Checked, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
instructions[2] = ir.Instruction{op=.Neg_Checked, type=types.I16, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
division_ops := [?]ir.Opcode{
.Div_Checked,
.Div_Trunc_Checked, .Div_Floor_Checked, .Div_Exact_Checked, .Div_Ceil_Checked,
.Rem_Checked, .Mod_Checked,
}
for op, index in division_ops {
instructions[3+index] = ir.Instruction{op=op, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
}
instructions[10] = ir.Instruction{op=.Return, type=types.I32, a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
append(&module.functions, ir.Function{
link_name=strings.clone("main"),
calling_convention=.C,
implementation=.Definition,
linkage=.External,
is_main=true,
result=types.I32,
instructions=instructions,
})
source_file := source.Source{path="test.bro", text=""}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
text := llvm.emit(&module, &diagnostics, &symbols)
defer delete(text)
testing.expect(t, strings.contains(text, "call void @bro.trap"))
testing.expect(t, strings.contains(text, "%v0 = add i8 0, -86"))
testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766"))
testing.expect(t, strings.contains(text, "%v2 = add i16 0, -21846"))
for index in 3..=9 {
testing.expect(t, strings.contains(text, fmt.tprintf("%%v%d = add i32 0, -1431655766", index)))
}
testing.expect(t, strings.contains(text, "@bro.trap(ptr %message, i64 %length) noreturn"))
testing.expect(t, !strings.contains(text, "%v-1"))
llvm_path := "/tmp/brolang-test-malformed-recovery.ll"
output := "/tmp/brolang-test-malformed-recovery"
defer _ = os.remove(llvm_path)
defer _ = os.remove(output)
testing.expect(t, os.write_entire_file(llvm_path, transmute([]byte)text))
testing.expect(t, backend.compile(llvm_path, output))
}
@(test)
hundred_thousand_term_runtime_addition_uses_iterative_pipeline :: proc(t: ^testing.T) {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
strings.write_string(&builder, "sum func(value i32) i32 { return value")
for _ in 0..<100_000 {
strings.write_string(&builder, " + 1")
}
strings.write_string(&builder, " }\nmain func() void { _ = sum(0) }\n")
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
instruction_count := 0
for function in ir_module.functions {
instruction_count += len(function.instructions)
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, instruction_count > 100_000)
}
@(test)
global_cycle_diagnostic_reports_endpoint_lines :: proc(t: ^testing.T) {
text := `a i32 :: b
b i32 :: a
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for _, diagnostic_index in diagnostics.items {
message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index))
found = found || strings.contains(message, "global initialization cycle from 'a' at test.bro:1 to 'b' at test.bro:2")
delete(message)
}
testing.expect(t, found)
}
@(test)
deep_global_cycle_detection_uses_iterative_dfs :: proc(t: ^testing.T) {
count := 50_000
ast_module := ast.init_module()
defer ast.destroy_module(&ast_module)
source_file := source.Source{path="test.bro", text=""}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
name := symbol.intern(&symbols, "value")
state := checker.Checker{
ast_module=&ast_module,
diagnostics=&diagnostics,
symbols=&symbols,
module=hir.init_module(),
allocator=context.allocator,
}
defer hir.destroy_module(&state.module)
defer delete(state.cycle_stack)
for id in 0..<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_stops_before_backend :: 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)
testing.expect(t, !os.exists(output))
}
@(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)
inferred_string_global_decays_to_demanded_slice :: proc(t: ^testing.T) {
text := "consume func(value []u8) usize { return value.len }\n" +
"program ::\n\t`abc\n" +
"main func() i32 {\n\tif consume(program) == 3 { return 0 }\n\treturn 1\n}\n"
directory := "/tmp/brolang-test-inferred-string-global"
main_path := "/tmp/brolang-test-inferred-string-global/main.bro"
output := "/tmp/brolang-test-inferred-string-global-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
mutable_runtime_globals_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-mutable-global"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/mutable_global", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 52)
}
@(test)
mutable_globals_infer_constraints_and_emit_writable_storage :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
}
counter int = 0
ratio float = 1
span range = 0..2
point Point = Point { x = 1 }
unset Point = undefined
values [_]mut i32 = [10, 20]
main func() void {
counter = 1
counter += 1
point.x = counter
values[1] = point.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)
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_counter := false
found_ratio := false
found_span := false
found_unset := false
found_values := false
for global, global_index in hir_module.globals {
name := symbol.resolve(&symbols, global.name)
if name == "counter" {
found_counter = global.writable && !global.is_static && types.equal(global.type, types.I32)
} else if name == "ratio" {
found_ratio = global.writable && !global.is_static && types.equal(global.type, types.F64)
} else if name == "span" {
found_span = global.writable && !global.is_static && types.is_range(global.type, &hir_module.types)
} else if name == "unset" {
found_unset = global.writable &&
!global.is_static &&
hir_module.exprs[global.expr].kind == .Undefined &&
len(ir_module.globals[global_index].initializer) > 0 &&
ir_module.globals[global_index].initializer[0].op == .Poison
} else if name == "values" {
item, ok := types.node(&hir_module.types, global.type)
found_values = global.writable && !global.is_static && ok && item.kind == .Array && item.count == 2
}
}
testing.expect(t, found_counter)
testing.expect(t, found_ratio)
testing.expect(t, found_span)
testing.expect(t, found_unset)
testing.expect(t, found_values)
testing.expect(t, strings.contains(llvm_text, "internal global"))
testing.expect(t, !strings.contains(llvm_text, "internal constant i32 0"))
}
@(test)
mutable_global_diagnostics_and_no_shadowing :: proc(t: ^testing.T) {
text := `ID :: distinct i32
OtherID :: distinct i32
ID i32 = 0
counter = 0
runtime i32 = 1
immutable :: 1
foo func(foo i32) void {}
main func() void {
_ = $runtime
immutable = 2
OtherID i32 = 0
local i32 = 0
if true {
local i32 = 1
}
for 0..1 |local| {}
scope i32 = 0
scope: {}
mark: {
mark i32 = 0
}
value i32 = value_label: {
value_label i32 = 1
yield :value_label 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)
missing_type := false
not_comptime := false
immutable_write := false
global_type_shadow := false
param_shadow := false
local_type_shadow := false
local_shadow := false
capture_shadow := false
label_shadow := false
local_label_shadow := false
value_label_shadow := false
for diagnostic in diagnostics.items {
missing_type = missing_type || strings.contains(diagnostic.message, "mutable global 'counter' requires a type annotation")
not_comptime = not_comptime || strings.contains(diagnostic.message, "global 'runtime' is not comptime-known")
immutable_write = immutable_write || strings.contains(diagnostic.message, "assignment target is not writable")
global_type_shadow = global_type_shadow || strings.contains(diagnostic.message, "global 'ID' shadows visible type")
param_shadow = param_shadow || strings.contains(diagnostic.message, "parameter 'foo' shadows visible function")
local_type_shadow = local_type_shadow || strings.contains(diagnostic.message, "local 'OtherID' shadows visible type")
local_shadow = local_shadow || strings.contains(diagnostic.message, "local 'local' shadows visible local")
capture_shadow = capture_shadow || strings.contains(diagnostic.message, "capture 'local' shadows visible local")
label_shadow = label_shadow || strings.contains(diagnostic.message, "label 'scope' shadows visible local")
local_label_shadow = local_label_shadow || strings.contains(diagnostic.message, "local 'mark' shadows visible label")
value_label_shadow = value_label_shadow || strings.contains(diagnostic.message, "local 'value_label' shadows visible label")
}
testing.expect(t, missing_type)
testing.expect(t, not_comptime)
testing.expect(t, immutable_write)
testing.expect(t, global_type_shadow)
testing.expect(t, param_shadow)
testing.expect(t, local_type_shadow)
testing.expect(t, local_shadow)
testing.expect(t, capture_shadow)
testing.expect(t, label_shadow)
testing.expect(t, local_label_shadow)
testing.expect(t, value_label_shadow)
}
@(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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(test)
malformed_typed_values_stop_before_backend :: 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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(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_import_is_a_warning :: 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/unused/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 {
if strings.contains(diagnostic.message, "unused import 'math'") {
found = true
testing.expect_value(t, diagnostic.severity, source.Severity.Warning)
formatted := source.format(&diagnostics, source.diagnostic_id(index))
testing.expect(t, strings.contains(formatted, "warning: unused import 'math'"))
delete(formatted)
}
}
testing.expect(t, found)
}
@(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)
testing.expect(t, !os.exists(output))
}
@(test)
referenced_missing_package_stops_before_backend :: 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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(test)
hide_is_rejected_outside_named_top_level_declarations :: proc(t: ^testing.T) {
cases := [?]string{
`hide import "../dep"`,
`hide dep :: import "../dep"`,
`hide func() void {}`,
`main func(hide value i32) void {}`,
`Box :: struct { hide i32 }`,
`main func() void { hide value i32 = 1 }`,
}
for text in cases {
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer 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)
hide_declarations_are_package_hidden :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-hidden-declarations"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/hidden_valid/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 9)
}
@(test)
package_hidden_declarations_resolve_locally_but_not_through_imports :: proc(t: ^testing.T) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load("examples/packages/hidden_invalid/app", &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
found_sibling := false
found_sibling_value := false
found_sibling_type := false
found_import := false
found_collision := false
for diagnostic in diagnostics.items {
found_sibling = found_sibling || strings.contains(diagnostic.message, "unknown symbol 'sibling'")
found_sibling_value = found_sibling_value || strings.contains(diagnostic.message, "unknown symbol 'sibling_value'")
found_sibling_type = found_sibling_type || strings.contains(diagnostic.message, "unknown or opaque record type 'Sibling'")
found_import = found_import || strings.contains(diagnostic.message, "package 'dep' has no member 'secret'")
found_collision = found_collision || strings.contains(diagnostic.message, "duplicate function 'collision'")
}
testing.expect(t, !found_sibling)
testing.expect(t, !found_sibling_value)
testing.expect(t, !found_sibling_type)
testing.expect(t, found_import)
testing.expect(t, found_collision)
}
@(test)
package_import_cycles_are_valid :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-cycle"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/cycle/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 4)
}
@(test)
imported_main_is_an_ordinary_callable_function :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-imported-main"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/imported_main/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 7)
}
@(test)
same_named_c_functions_in_packages_do_not_collide :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-c-symbols"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/c_symbols/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
invalid_root_package_preserves_existing_output :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-preserved-output"
defer _ = os.remove(output)
previous := "previous artifact"
testing.expect(t, os.write_entire_file(output, transmute([]byte)previous))
testing.expect_value(t, compiler_core.compile_package("examples/programs/prototype/main.bro", output), 2)
data, ok := os.read_entire_file(output)
defer delete(data)
testing.expect(t, ok)
testing.expect_value(t, string(data), previous)
}
@(test)
empty_root_package_is_an_infrastructure_error :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-empty"
defer _ = os.remove(output)
testing.expect_value(t, compiler_core.compile_package("examples/packages/empty", output), 2)
testing.expect(t, !os.exists(output))
}
@(test)
same_package_can_be_imported_under_distinct_aliases :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-aliases"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/aliases/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 4)
}
@(test)
duplicate_same_file_import_stops_before_backend :: 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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(test)
declaration_aliases_preserve_identity_and_chain :: proc(t: ^testing.T) {
root :: "/tmp/brolang-test-declaration-aliases"
dep_dir :: root + "/dep"
facade_dir :: root + "/facade"
top_dir :: root + "/top"
app_dir :: root + "/app"
output :: "/tmp/brolang-test-declaration-aliases-output"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
defer _ = os.remove(output)
directories := [?]string{root, dep_dir, facade_dir, top_dir, app_dir}
for directory in directories {
testing.expect(t, os.make_directory(directory) == nil)
}
dep_text := `Box func($T type) type {
return struct { value T }
}
Point :: struct { value i32 }
counter i32 = 1
answer func() i32 { return 40 }
`
facade_text := `dep :: import "../dep"
RenamedBox :: alias dep.Box
RenamedPoint :: alias dep.Point
counter :: alias dep.counter
answer :: alias dep.answer
local_answer func() i32 { return local_answer_alias() }
Scalar :: alias i32
ScalarChain :: alias Scalar
static_scalar Scalar :: Scalar(usize(6))
MaybePoint :: alias ?@dep.Point
Concrete :: alias dep.Box(i32)
`
hidden_text := `dep :: import "../dep"
hide local_answer_alias :: alias dep.answer
`
top_text := `facade :: import "../facade"
Box :: alias facade.RenamedBox
Point :: alias facade.RenamedPoint
counter :: alias facade.counter
answer :: alias facade.answer
`
app_text := `dep :: import "../dep"
facade :: import "../facade"
top :: import "../top"
apply func($callback func() i32) i32 { return callback() }
main func() i32 {
box top.Box(i32) :: top.Box(i32) { value = 2 }
point top.Point :: top.Point { value = 3 }
maybe facade.MaybePoint :: null
scalar facade.Scalar :: facade.Scalar(usize(5))
chained facade.ScalarChain :: facade.ScalarChain(usize(6))
top.counter = 7
if box.value != 2 or point.value != 3 { return 1 }
if scalar != 5 or chained != 6 or facade.static_scalar != 6 or top.answer() != 40 or facade.local_answer() != 40 or dep.counter != 7 { return 2 }
if apply(top.answer) != 40 or apply(facade.answer) != 40 { return 3 }
_ = maybe
return 0
}
`
testing.expect(t, os.write_entire_file(dep_dir + "/dep.bro", transmute([]byte)dep_text))
testing.expect(t, os.write_entire_file(facade_dir + "/facade.bro", transmute([]byte)facade_text))
testing.expect(t, os.write_entire_file(facade_dir + "/hidden.bro", transmute([]byte)hidden_text))
testing.expect(t, os.write_entire_file(top_dir + "/top.bro", transmute([]byte)top_text))
testing.expect(t, os.write_entire_file(app_dir + "/main.bro", transmute([]byte)app_text))
status := compiler_core.compile_package(app_dir, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
scalar_alias_casts_reject_bad_arity_and_operands :: proc(t: ^testing.T) {
text := `StringId :: alias u32
main func() void {
_ = StringId()
_ = StringId(1, 2)
_ = StringId(true)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
arity := 0
bad_operand := false
for diagnostic in diagnostics.items {
arity += 1 if strings.contains(diagnostic.message, "type alias 'StringId' expects 1 argument") else 0
bad_operand = bad_operand || strings.contains(diagnostic.message, "scalar cast requires numeric scalar types")
}
testing.expect_value(t, arity, 2)
testing.expect(t, bad_operand)
}
@(test)
optional_contextual_enum_literals_compile_at_runtime_and_comptime :: proc(t: ^testing.T) {
text := `Kind :: enum { first, second }
Choice :: union(enum) { empty void, payload i32 }
STATIC ?Kind :: .second
STATIC_CHOICE ?Choice :: .payload{4}
choose func(first bool) ?Kind {
if first { return .first }
return .second
}
choose_choice func() ?Choice { return .empty }
main func() i32 {
if choose(true) |value| {
if value != Kind.first { return 1 }
} else { return 2 }
if STATIC |value| {
if value != Kind.second { return 3 }
} else { return 4 }
if choose_choice() |_| {} else { return 5 }
if STATIC_CHOICE |_| {} else { return 6 }
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
declaration_aliases_diagnose_invalid_targets :: proc(t: ^testing.T) {
root :: "/tmp/brolang-test-declaration-alias-errors"
dep_dir :: root + "/dep"
facade_dir :: root + "/facade"
a_dir :: root + "/a"
b_dir :: root + "/b"
app_dir :: root + "/app"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
directories := [?]string{root, dep_dir, facade_dir, a_dir, b_dir, app_dir}
for directory in directories {
testing.expect(t, os.make_directory(directory) == nil)
}
dep_text := `visible func() i32 { return 1 }
hide hidden_target func() i32 { return 2 }
ambiguous func() i32 { return 3 }
ambiguous i32 :: 4
`
facade_text := `dep :: import "../dep"
gone :: import "../gone"
missing :: alias dep.missing
hidden :: alias dep.hidden_target
unknown :: alias nope.visible
unavailable :: alias gone.visible
ambiguous :: alias dep.ambiguous
duplicate :: alias dep.visible
duplicate :: alias dep.visible
collision func() i32 { return 0 }
collision :: alias dep.visible
dep :: alias dep.visible
`
a_text := `b :: import "../b"
value :: alias b.value
`
b_text := `a :: import "../a"
value :: alias a.value
`
app_text := `facade :: import "../facade"
a :: import "../a"
main func() void {}
`
testing.expect(t, os.write_entire_file(dep_dir + "/dep.bro", transmute([]byte)dep_text))
testing.expect(t, os.write_entire_file(facade_dir + "/facade.bro", transmute([]byte)facade_text))
testing.expect(t, os.write_entire_file(a_dir + "/a.bro", transmute([]byte)a_text))
testing.expect(t, os.write_entire_file(b_dir + "/b.bro", transmute([]byte)b_text))
testing.expect(t, os.write_entire_file(app_dir + "/main.bro", transmute([]byte)app_text))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load(app_dir, &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
testing.expect(t, loaded)
wants := []string{
"has no member 'missing'",
"is package-hidden",
"unknown symbol 'nope'",
"unavailable imported package 'gone'",
"package member 'dep.ambiguous' is ambiguous",
"duplicate declaration alias 'duplicate'",
"declaration alias 'collision' conflicts with a package declaration",
"declaration alias 'dep' conflicts with an import",
"declaration alias cycle",
}
for want in wants {
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, want)
}
testing.expect(t, found)
}
}
@(test)
self_import_via_dot_is_valid :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-self"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/self_import/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 5)
}
@(test)
absolute_import_error_stops_before_backend :: 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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(test)
cross_package_generic_specializes_from_folded_argument :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-generic"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/generic/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 128)
}
@(test)
comptime_value_params_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-comptime-value-params"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/comptime_value_params", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
comptime_type_params_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-comptime-type-params"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/comptime_type_params", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
type_factories_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-type-factory"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/type_factory", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
arraylist_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-arraylist"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/arraylist", output, nil, target.DEFAULT, cimport.Options{}, ".")
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
type_factory_rejects_runtime_parameters_and_recursion :: proc(t: ^testing.T) {
texts := []string{
`Bad func($T type, n usize) type {
return struct { value [n]T }
}
main func() void { value Bad(i32, 4) = undefined; _ = &value }
`,
`Loop func($T type) type {
return struct { next @Loop(T) }
}
main func() void { value Loop(i32) = undefined; _ = &value }
`,
`Box func($T type) type {
return struct { value T }
}
main func() void { _ = Box(i32) }
`,
`Bad func($T type) type {
return 1
}
main func() void { value Bad(i32) = undefined; _ = &value }
`,
}
wanted := []string{"must be comptime", "recursive type-factory specialization", "only valid in type position", "cannot implicitly convert"}
for text, index in texts {
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, wanted[index])
}
testing.expect(t, found)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
comptime_eval_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-comptime-eval"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/comptime_eval", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
comptime_v1_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-comptime-v1"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/comptime_v1", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
lazy_function_body_marks_import_as_used_without_resolving_it :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-lazy-import"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/lazy_import/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
qualified_global_inference_ignores_same_named_local :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-qualified-shadow"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/qualified_shadow/app", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 300)
}
@(test)
cross_package_recursive_specialization_reaches_fixed_point :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-recursive"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/recursive/app", output)
testing.expect_value(t, status, 0)
testing.expect(t, os.exists(output))
}
@(test)
imported_main_does_not_satisfy_root_main_requirement :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-package-missing-root-main"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/packages/missing_root_main/app", output)
testing.expect_value(t, status, 1)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(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_stops_before_backend :: 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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(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)
testing.expect(t, !os.exists(output))
}
@(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)
exhaustive_enum_guard_sequences_return :: proc(t: ^testing.T) {
text := `E :: enum { a, b }
complete func(value E) i32 {
if (value == .a) { return 1 }
if (value == .b) { return 2 }
}
incomplete func(value E) i32 {
if (value == .a) { return 1 }
}
main func() void {
_ = complete(.a)
_ = incomplete(.a)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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_returns := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "does not return a value") {
missing_returns += 1
}
}
testing.expect_value(t, missing_returns, 1)
}
@(test)
conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-conditional-unwrap"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/conditional_unwrap", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
// Single unwrap, guarded two/three-value unwraps, optional pointers, false
// guards, and failed short-circuit chains preserve the expected total.
testing.expect_value(t, state.exit_code, 42)
}
@(test)
conditional_unwrap_parser_captures_guard_and_parenthesized_chain :: proc(t: ^testing.T) {
text := `main func() void {
first ?i32 = 1
second ?i32 = 2
if (first and second) |a, b : a == 1 and b == 2| {
_ = a
_ = b
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
statement := module.statements[module.functions[0].body[2]]
testing.expect_value(t, statement.kind, ast.Stmt_Kind.If)
testing.expect_value(t, len(statement.captures), 2)
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.And)
testing.expect(t, module.exprs[statement.expr].parenthesized)
testing.expect(t, statement.guard != ast.INVALID_EXPR)
testing.expect_value(t, module.exprs[statement.guard].kind, ast.Expr_Kind.And)
}
@(test)
parser_accepts_braceless_if_bodies :: proc(t: ^testing.T) {
text := `ready func() bool { return true }
main func() void {
x i32 = 0
if (x == 0) x = 1
if ready() x = 2
if (x == 2) x = 3 else x = 4
if (x > 0) { x = 10 } else x = 11
v ?i32 = 5
if (v) |u| _ = u
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
main := module.functions[1]
testing.expect_value(t, len(main.body), 7)
paren_if := module.statements[main.body[1]]
testing.expect_value(t, paren_if.kind, ast.Stmt_Kind.If)
testing.expect_value(t, len(paren_if.body), 1)
testing.expect(t, module.exprs[paren_if.expr].parenthesized)
call_if := module.statements[main.body[2]]
testing.expect_value(t, call_if.kind, ast.Stmt_Kind.If)
testing.expect_value(t, len(call_if.body), 1)
testing.expect_value(t, module.exprs[call_if.expr].kind, ast.Expr_Kind.Call)
if_else := module.statements[main.body[3]]
testing.expect_value(t, len(if_else.body), 1)
testing.expect_value(t, len(if_else.else_body), 1)
braced_then := module.statements[main.body[4]]
testing.expect_value(t, len(braced_then.body), 1)
testing.expect_value(t, len(braced_then.else_body), 1)
unwrap_if := module.statements[main.body[6]]
testing.expect_value(t, len(unwrap_if.captures), 1)
testing.expect_value(t, len(unwrap_if.body), 1)
}
@(test)
match_else_arm_is_not_captured_by_braceless_if :: proc(t: ^testing.T) {
text := `ready func() bool { return true }
main func() void {
value i32 = 0
match value {
0: if ready() _ = value
else: _ = 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)
match_statement := module.statements[module.functions[1].body[1]]
testing.expect_value(t, len(match_statement.body), 2)
first_arm := module.statements[match_statement.body[0]]
first_if := module.statements[first_arm.body[0]]
testing.expect_value(t, first_if.kind, ast.Stmt_Kind.If)
testing.expect_value(t, len(first_if.else_body), 0)
else_arm := module.statements[match_statement.body[1]]
testing.expect_value(t, len(else_arm.patterns), 0)
}
@(test)
parser_diagnoses_braceless_if_without_parens_or_call :: proc(t: ^testing.T) {
text := `main func() void {
x i32 = 0
if x == 0 x = 1
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 1)
testing.expect(t, strings.contains(diagnostics.items[0].message, "parenthesized"))
}
@(test)
parser_separates_contextual_enum_literal_from_if_block :: proc(t: ^testing.T) {
text := `Kind :: enum {
newline
other
}
main func() void {
kind Kind = .other
if kind != .newline {
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer 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.statements[module.functions[0].body[1]].kind, ast.Stmt_Kind.If)
}
@(test)
braceless_value_if_infers_shorthand_enum_from_use :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-value-if-enum"
main_path := "/tmp/brolang-test-value-if-enum/main.bro"
output := "/tmp/brolang-test-value-if-enum-output"
text := `Kind :: enum { first, second }
Box :: struct { kind Kind }
choose func(first bool) Kind {
kind :: if (first) .first else .second
return Box{kind = kind}.kind
}
main func() i32 {
if (choose(true) != .first) return 1
if (choose(false) != .second) return 2
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
parser_accepts_braceless_while_bodies :: proc(t: ^testing.T) {
text := `ready func() bool { return false }
main func() void {
cursor i32 = 0
while (cursor < 1) cursor += 1
while ready() cursor += 1
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
main := module.functions[1]
testing.expect_value(t, len(main.body), 3)
parenthesized := module.statements[main.body[1]]
testing.expect_value(t, parenthesized.kind, ast.Stmt_Kind.While)
testing.expect_value(t, len(parenthesized.body), 1)
testing.expect(t, module.exprs[parenthesized.expr].parenthesized)
call := module.statements[main.body[2]]
testing.expect_value(t, call.kind, ast.Stmt_Kind.While)
testing.expect_value(t, len(call.body), 1)
testing.expect_value(t, module.exprs[call.expr].kind, ast.Expr_Kind.Call)
}
@(test)
parser_diagnoses_braceless_while_without_parens_or_call :: proc(t: ^testing.T) {
text := `main func() void {
cursor i32 = 0
limit i32 = 1
while cursor < limit cursor += 1
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 1)
testing.expect_value(
t,
diagnostics.items[0].message,
"a brace-less 'while' body requires the condition to be parenthesized unless it is a function call",
)
}
@(test)
braceless_while_compiles_and_runs :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-braceless-while"
main_path := "/tmp/brolang-test-braceless-while/main.bro"
output := "/tmp/brolang-test-braceless-while-output"
text := `main func() i32 {
cursor i32 = 0
while (cursor < 42) cursor += 1
return cursor
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
parser_diagnoses_braceless_for_and_unwrap_without_parens_or_call :: proc(t: ^testing.T) {
text := `main func() void {
for tokens.items |item| _ = item
value ?i32 = 1
if value |present| _ = present
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 2)
testing.expect(t, strings.contains(diagnostics.items[0].message, "brace-less 'for'"))
testing.expect(t, strings.contains(diagnostics.items[1].message, "brace-less 'if'"))
}
@(test)
braceless_if_compiles_and_runs :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-braceless-if"
main_path := "/tmp/brolang-test-braceless-if/main.bro"
output := "/tmp/brolang-test-braceless-if-output"
text := `ready func() bool { return true }
main func() i32 {
x i32 = 0
if (x == 0) x = 1 else x = 2
if ready() x = x + 10
y i32 = 5
if (y == 0) y = 1 else y = 30
x = x + y
return x
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 41)
}
@(test)
conditional_unwrap_allows_sink_captures :: proc(t: ^testing.T) {
text := `main func() void {
first ?i32 = 1
second ?i32 = 2
if first and second |_, value : value == 2| {
_ = value
}
if first |_| {}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
main := hir_module.functions[0]
first_if := hir_module.statements[main.body[2]]
second_if := hir_module.statements[main.body[3]]
testing.expect_value(t, first_if.unwraps[0].local, hir.INVALID_LOCAL)
testing.expect(t, first_if.unwraps[1].local != hir.INVALID_LOCAL)
testing.expect_value(t, second_if.unwraps[0].local, hir.INVALID_LOCAL)
}
@(test)
parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^testing.T) {
cases := [4]struct {
text: string,
needle: string,
}{
{`main func() void {
value ?i32 = 1
if value || {}
}
`, "expected an unwrap capture name"},
{`main func() void {
value ?i32 = 1
if value |capture,| {}
}
`, "expected an unwrap capture after ','"},
{`main func() void {
value ?i32 = 1
if value |capture :| {}
}
`, "expected a guard expression after ':'"},
{`main func() void {
value ?i32 = 1
if value |capture {}
}
`, "expected '|' to close unwrap captures"},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
module := parser.parse(&stream, &source_file, &diagnostics)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.needle)
}
testing.expect(t, found)
ast.destroy_module(&module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
if_unwrap_on_non_optional_is_diagnosed :: proc(t: ^testing.T) {
text := `main func() i32 {
x i32 = 5
if x |v| {
return v
}
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "unwrap requires an optional")
}
testing.expect(t, found)
}
@(test)
conditional_unwrap_diagnostics_cover_counts_guards_and_capture_scope :: proc(t: ^testing.T) {
text := `main func() void {
first ?i32 = 1
second ?i32 = 2
plain i32 = 3
if first and second |one| {}
if first |one, two| {}
if first and second |same, same| {}
if plain |value| {}
if first |value : value| {}
if first and earlier |earlier, later| {}
if first |value| {
value = 2
}
if first |value| {
value i32 = 2
_ = value
}
if first |value| {
_ = value
} else {
_ = value
}
_ = value
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
count_mismatches := 0
duplicate := false
non_optional := false
guard := false
outer_operand_scope := false
immutable := false
redeclaration := false
capture_scope := 0
for diagnostic in diagnostics.items {
count_mismatches += 1 if strings.contains(diagnostic.message, "unwrap has") else 0
duplicate = duplicate || strings.contains(diagnostic.message, "unwrap captures must have distinct names")
non_optional = non_optional || strings.contains(diagnostic.message, "unwrap requires an optional value")
guard = guard || strings.contains(diagnostic.message, "unwrap guard must be a bool")
outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unknown symbol 'earlier'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'value'")
capture_scope += 1 if strings.contains(diagnostic.message, "unknown symbol 'value'") else 0
}
testing.expect_value(t, count_mismatches, 2)
testing.expect(t, duplicate)
testing.expect(t, non_optional)
testing.expect(t, guard)
testing.expect(t, outer_operand_scope)
testing.expect(t, immutable)
testing.expect(t, redeclaration)
testing.expect_value(t, capture_scope, 2)
}
@(test)
if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) {
// The binding `v` is usable in the then-block but not in the else-block.
text := `main func() i32 {
a ?i32 = 1
if a |v| {
return v
} else {
return v
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "unknown symbol 'v'")
}
testing.expect(t, found)
}
@(test)
if_unwrap_binding_is_immutable :: proc(t: ^testing.T) {
text := `main func() i32 {
a ?i32 = 1
if a |v| {
v = 2
return v
}
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "cannot assign immutable local 'v'")
}
testing.expect(t, found)
}
@(test)
while_loops_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-while-loop"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/while_loop", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
while_loop_diagnostics_cover_condition_update_and_scope :: proc(t: ^testing.T) {
text := `bad_condition func() void {
while 1 {}
}
bad_unresolved func() void {
while false : missing = 1 {}
}
bad_immutable func() void {
i :: 0
while false : i = i + 1 {}
}
bad_body_scope func() void {
running :: false
while running : i = 1 {
i u32 = 0
}
}
bad_declaration_update func() void {
while false : i u32 = 0 {}
}
bad_missing_update func() void {
while false : {}
}
main func() void {
bad_condition()
bad_unresolved()
bad_immutable()
bad_body_scope()
bad_declaration_update()
bad_missing_update()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
non_bool := false
unresolved_missing := false
unresolved_body_local := false
immutable := false
disallowed := false
missing := false
for diagnostic in diagnostics.items {
non_bool = non_bool || strings.contains(diagnostic.message, "'while' condition must be a bool")
unresolved_missing = unresolved_missing || strings.contains(diagnostic.message, "cannot assign unresolved local 'missing'")
unresolved_body_local = unresolved_body_local || strings.contains(diagnostic.message, "cannot assign unresolved local 'i'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'i'")
disallowed = disallowed || strings.contains(diagnostic.message, "while update must be an assignment, sink, or expression statement")
missing = missing || strings.contains(diagnostic.message, "expected a while update statement after ':'")
}
testing.expect(t, non_bool)
testing.expect(t, unresolved_missing)
testing.expect(t, unresolved_body_local)
testing.expect(t, immutable)
testing.expect(t, disallowed)
testing.expect(t, missing)
}
@(test)
while_true_and_potential_fallthrough_have_distinct_return_analysis :: proc(t: ^testing.T) {
text := `forever func() i32 {
while true {}
}
maybe func(run bool) i32 {
while run {
return 1
}
}
main func() void {
if false {
_ = forever()
}
_ = maybe(false)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
missing_return_count := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "does not return a value") {
missing_return_count += 1
}
}
testing.expect_value(t, missing_return_count, 1)
}
@(test)
while_loop_allocas_are_emitted_in_the_entry_block :: proc(t: ^testing.T) {
text := `main func() i32 {
i u32 = 0
while i < 2 and true : i = i + 1 {
value u32 = i
_ = value
}
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
testing.expect(t, first_loop_label >= 0)
alloca_count := 0
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction, instruction_index in function.instructions {
if instruction.op != .Alloca {
continue
}
alloca_count += 1
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
offset := find_substring_offset(llvm_text, needle)
testing.expect(t, offset >= 0 && offset < first_loop_label)
}
}
testing.expect(t, alloca_count >= 3)
}
@(test)
for_loop_tokens_and_parser_capture_range_shape :: proc(t: ^testing.T) {
text := `main func() void {
for 0..4 |value| {
_ = value
}
items [1]mut i32 = [1]
for (&items) |@item, index| {
_ = item
_ = index
}
for 0..=1 |inclusive| {
_ = inclusive
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
for_count := 0
range_count := 0
inclusive_count := 0
for tok in stream.items {
#partial switch tok.kind {
case .Keyword_For: for_count += 1
case .Range: range_count += 1
case .Range_Inclusive: inclusive_count += 1
case:
}
}
testing.expect_value(t, for_count, 3)
testing.expect_value(t, range_count, 1)
testing.expect_value(t, inclusive_count, 1)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
function := module.functions[0]
first := module.statements[function.body[0]]
second := module.statements[function.body[2]]
third := module.statements[function.body[3]]
testing.expect_value(t, first.kind, ast.Stmt_Kind.For)
testing.expect(t, !first.pointer_capture)
testing.expect_value(t, module.exprs[first.expr].kind, ast.Expr_Kind.Range)
testing.expect_value(t, module.exprs[first.expr].integer, u64(0))
testing.expect_value(t, second.kind, ast.Stmt_Kind.For)
testing.expect(t, second.pointer_capture)
testing.expect(t, symbol.is_valid(second.index_name))
testing.expect_value(t, third.kind, ast.Stmt_Kind.For)
testing.expect_value(t, module.exprs[third.expr].integer, u64(1))
}
@(test)
for_loop_sink_captures_are_throwaways :: proc(t: ^testing.T) {
text := `main func() void {
for 0..1 |_| {}
for [1] |_, _| {}
for 0..1 |unused_capture| {}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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)
found_unused_capture := false
warning_count := 0
error_count := 0
for diagnostic in diagnostics.items {
if diagnostic.severity == source.Severity.Warning {
warning_count += 1
} else {
error_count += 1
}
found_unused_capture =
found_unused_capture ||
strings.contains(diagnostic.message, "unused local 'unused_capture'")
testing.expect(t, !strings.contains(diagnostic.message, "unused local '_'"))
testing.expect(t, !strings.contains(diagnostic.message, "distinct names"))
testing.expect(t, !strings.contains(diagnostic.message, "expected a for-loop"))
}
testing.expect_value(t, warning_count, 1)
testing.expect_value(t, error_count, 0)
testing.expect(t, found_unused_capture)
}
@(test)
parser_accepts_braceless_for_bodies :: proc(t: ^testing.T) {
text := `make_items func() [2]i32 { return [1, 2] }
main func() void {
for (tokens.items) |item| _ = item
for make_items() |item|
_ = item
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
main := module.functions[1]
testing.expect_value(t, len(main.body), 2)
parenthesized := module.statements[main.body[0]]
call := module.statements[main.body[1]]
testing.expect_value(t, len(parenthesized.body), 1)
testing.expect(t, module.exprs[parenthesized.expr].parenthesized)
testing.expect_value(t, len(call.body), 1)
testing.expect_value(t, module.exprs[call.expr].kind, ast.Expr_Kind.Call)
}
@(test)
braceless_for_compiles_and_runs :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-braceless-for"
main_path := "/tmp/brolang-test-braceless-for/main.bro"
output := "/tmp/brolang-test-braceless-for-output"
text := `make_items func() [2]i32 { return [20, 2] }
main func() i32 {
total i32 = 0
items :: [10, 11]
for (items) |item| total += item
for make_items() |item|
total += item
return total
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 43)
}
@(test)
range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) {
text := `limit_func func() usize { return 3 }
main func() void {
limit :: 3
for 0..limit + 1 |bad| {
_ = bad
}
for 0..(limit + 1) |good| {
_ = good
}
for 0..=usize(limit) |cast_bound| {
_ = cast_bound
}
for 0..limit_func() |call_bound| {
_ = call_bound
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found := 0
for diagnostic in diagnostics.items {
found += 1 if strings.contains(diagnostic.message, "range bounds with operators must be parenthesized") else 0
}
testing.expect_value(t, found, 1)
}
@(test)
parser_diagnoses_malformed_for_captures :: proc(t: ^testing.T) {
cases := [4]struct {
text: string,
needle: string,
}{
{`main func() void {
for [1] item {}
}
`, "expected '|' before for-loop captures"},
{`main func() void {
for [1] |@| {}
}
`, "expected a for-loop item capture"},
{`main func() void {
for [1] |item,| {}
}
`, "expected an index capture after ','"},
{`main func() void {
for [1] |item {}
}
`, "expected '|' to close for-loop captures"},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
module := parser.parse(&stream, &source_file, &diagnostics)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.needle)
}
testing.expect(t, found)
ast.destroy_module(&module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
for_loops_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-for-loop"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/for_loop", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
range_loop_edges_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-for-loop-edges"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/for_loop_edges", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
for_loop_diagnostics_cover_iterables_captures_and_scope :: proc(t: ^testing.T) {
text := `bad_iterable func() void {
for 1 |item| {
_ = item
}
}
bad_array_pointer_capture func() void {
for [1] |@item| {
_ = item
}
}
bad_range_pointer_capture func() void {
for 0..1 |@item| {
_ = item
}
}
bad_range_index_capture func() void {
for 0..1 |item, index| {
_ = item
_ = index
}
}
bad_duplicate_capture func() void {
for [1] |item, item| {
_ = item
}
}
bad_capture_redeclaration func() void {
for [1] |item| {
item i32 = 2
_ = item
}
}
bad_capture_assignment func() void {
for [1] |item| {
item = 2
}
}
bad_immutable_pointer_capture func() void {
items :: [1]
for (&items) |@item| {
item^ = 2
}
}
bad_scope func() void {
for [1] |item| {
_ = item
}
_ = item
}
bad_integer_bounds func() void {
start i32 = 0
end u32 = 1
for start..end |item| {
_ = item
}
}
bad_float_bounds func() void {
for 0.0..1.0 |item| {
_ = item
}
}
main func() void {
bad_iterable()
bad_array_pointer_capture()
bad_range_pointer_capture()
bad_range_index_capture()
bad_duplicate_capture()
bad_capture_redeclaration()
bad_capture_assignment()
bad_immutable_pointer_capture()
bad_scope()
bad_integer_bounds()
bad_float_bounds()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
unsupported := false
array_pointer := false
range_pointer := false
range_index := false
duplicate_capture := false
redeclaration := false
immutable := false
immutable_pointer := false
scope := false
integer_bounds := 0
for diagnostic in diagnostics.items {
unsupported = unsupported || strings.contains(diagnostic.message, "for-loop iterable must be a range")
array_pointer = array_pointer || strings.contains(diagnostic.message, "pointer capture over an array requires")
range_pointer = range_pointer || strings.contains(diagnostic.message, "range loops do not support pointer captures")
range_index = range_index || strings.contains(diagnostic.message, "range loops do not support index captures")
duplicate_capture = duplicate_capture || strings.contains(diagnostic.message, "for-loop captures must have distinct names")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'item'")
immutable_pointer = immutable_pointer || strings.contains(diagnostic.message, "assignment target is not writable")
scope = scope || strings.contains(diagnostic.message, "unknown symbol 'item'")
integer_bounds += 1 if strings.contains(diagnostic.message, "range bounds must be compatible concrete integers") else 0
}
testing.expect(t, unsupported)
testing.expect(t, array_pointer)
testing.expect(t, range_pointer)
testing.expect(t, range_index)
testing.expect(t, duplicate_capture)
testing.expect(t, redeclaration)
testing.expect(t, immutable)
testing.expect(t, immutable_pointer)
testing.expect(t, scope)
testing.expect_value(t, integer_bounds, 2)
}
@(test)
for_pointer_capture_respects_pointer_and_array_mutability :: proc(t: ^testing.T) {
text := `readonly func() void {
values [1]mut i32 = [1]
items @[1]mut i32 = &values
items[0] = 7
for items |@item| {
item^ = 7
}
}
writable func() void {
values [1]mut i32 = [1]
items @mut [1]mut i32 = &values
items[0] = 7
for items |@item| {
item^ = 7
}
}
main func() void {
readonly()
writable()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
readonly_errors := 0
for diagnostic in diagnostics.items {
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
}
testing.expect_value(t, readonly_errors, 2)
}
@(test)
pointer_field_passthrough_respects_pointee_mutability :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
}
readonly func(point @Point) i32 {
return point.x
}
bad_write func(point @Point) void {
point.x = 7
}
writable func(point @mut Point) void {
point.x += 1
}
main func() i32 {
point Point = Point { x = 41 }
writable(&point)
bad_write(&point)
return readonly(&point)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
readonly_errors := 0
for diagnostic in diagnostics.items {
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
}
testing.expect_value(t, readonly_errors, 1)
}
@(test)
equal_range_returns_infer_a_usable_result_type :: proc(t: ^testing.T) {
text := `choose func(first bool) range {
if first {
return 0..1
}
return 2..3
}
main func() i32 {
total i32 = 0
for choose(false) |value| {
total = total + value
}
return total
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
found := false
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) == "choose" {
found = true
testing.expect(t, types.is_range(function.result, &hir_module.types))
testing.expect_value(t, types.child_type(function.result, &hir_module.types), types.I8)
}
}
testing.expect(t, found)
testing.expect(t, strings.contains(llvm_text, "extractvalue"))
}
@(test)
for_loop_lowering_evaluates_once_and_avoids_index_bounds_checks :: proc(t: ^testing.T) {
text := `make_range func() range {
return 0..2
}
make_array func() [2]i32 {
return [1, 2]
}
main func() i32 {
total i32 = 0
for make_range() |value| {
total = total + value
}
for make_array() |value| {
total = total + value
}
return total
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
call_count := 0
extract_count := 0
select_count := 0
pointer_add_count := 0
index_address_count := 0
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
testing.expect(t, first_loop_label >= 0)
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction, instruction_index in function.instructions {
#partial switch instruction.op {
case .Call: call_count += 1
case .Extract: extract_count += 1
case .Select: select_count += 1
case .Pointer_Add: pointer_add_count += 1
case .Index_Address: index_address_count += 1
case .Alloca:
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
offset := find_substring_offset(llvm_text, needle)
testing.expect(t, offset >= 0 && offset < first_loop_label)
case:
}
}
}
// make_array() is a zero-runtime value call and is materialized directly.
testing.expect_value(t, call_count, 1)
testing.expect_value(t, extract_count, 3)
testing.expect_value(t, select_count, 2)
testing.expect(t, pointer_add_count >= 1)
testing.expect_value(t, index_address_count, 0)
testing.expect(t, !strings.contains(llvm_text, "index_ok"))
}
@(test)
lexer_emits_compound_assignment_and_slash_tokens :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="+= -= *= /= / *"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, stream.items[0].kind, token.Kind.Plus_Equal)
testing.expect_value(t, stream.items[1].kind, token.Kind.Minus_Equal)
testing.expect_value(t, stream.items[2].kind, token.Kind.Star_Equal)
testing.expect_value(t, stream.items[3].kind, token.Kind.Slash_Equal)
testing.expect_value(t, stream.items[4].kind, token.Kind.Slash)
testing.expect_value(t, stream.items[5].kind, token.Kind.Star)
}
@(test)
binary_operators_respect_multiplicative_precedence :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="value :: 1 + 2 * 3\nmain func() void {}\n"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
root := module.exprs[module.globals[0].expr]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, root.kind, ast.Expr_Kind.Add)
testing.expect_value(t, module.exprs[root.left].integer, u64(1))
testing.expect_value(t, module.exprs[root.right].kind, ast.Expr_Kind.Mul)
}
@(test)
division_parses_left_associatively :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="value :: 8 / 4 / 2\nmain func() void {}\n"}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
root := module.exprs[module.globals[0].expr]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, root.kind, ast.Expr_Kind.Div)
testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Div)
testing.expect_value(t, module.exprs[root.right].integer, u64(2))
}
@(test)
compound_assignment_preserves_operation_and_rhs :: proc(t: ^testing.T) {
text := `main func() void {
x i32 = 0
x += 5
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
body := module.functions[0].body
statement := module.statements[body[1]]
testing.expect_value(t, statement.kind, ast.Stmt_Kind.Assignment)
testing.expect_value(t, statement.assignment_op, ast.Assignment_Op.Add)
testing.expect(t, statement.target != ast.INVALID_EXPR)
testing.expect_value(t, module.exprs[statement.target].kind, ast.Expr_Kind.Name)
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Integer)
testing.expect_value(t, module.exprs[statement.expr].integer, u64(5))
}
@(test)
undefined_inferred_local_lowers_to_fill :: proc(t: ^testing.T) {
text := `choose func(flag bool) i32 {
value int = undefined
if flag {
value = 42
} else {
value = -2
}
return value
}
main func() i32 {
return choose(true)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
choose_symbol := symbol.intern(&symbols, "choose")
value_symbol := symbol.intern(&symbols, "value")
found_value_i32 := false
fill_count := 0
for function, function_index in hir_module.functions {
if function.name != choose_symbol {
continue
}
for local in function.locals {
found_value_i32 = found_value_i32 || local.name == value_symbol && local.type == types.I32
}
for instruction in ir_module.functions[function_index].instructions {
fill_count += 1 if instruction.op == .Fill else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_value_i32)
testing.expect_value(t, fill_count, 1)
testing.expect(t, strings.contains(llvm_text, "declare void @llvm.memset.p0.i64"))
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memset.p0.i64"))
testing.expect(t, strings.contains(llvm_text, "i8 -86"))
}
@(test)
local_int_inference_widens_from_assignments :: proc(t: ^testing.T) {
text := `wide func() int {
value int = 1
value = 1000
return value
}
main func() void {
_ = wide()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
wide_symbol := symbol.intern(&symbols, "wide")
value_symbol := symbol.intern(&symbols, "value")
found_value_i16 := false
found_result_i16 := false
for function in hir_module.functions {
if function.name != wide_symbol {
continue
}
found_result_i16 = function.result == types.I16
for local in function.locals {
found_value_i16 = found_value_i16 || local.name == value_symbol && local.type == types.I16
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_result_i16)
testing.expect(t, found_value_i16)
}
float_local_type :: proc(hir_module: ^hir.Module, symbols: ^symbol.Table, function_name, local_name: string) -> (types.Type, bool) {
fn_symbol := symbol.intern(symbols, function_name)
loc_symbol := symbol.intern(symbols, local_name)
for function in hir_module.functions {
if function.name != fn_symbol {
continue
}
for local in function.locals {
if local.name == loc_symbol {
return local.type, true
}
}
}
return types.INVALID, false
}
float_result_type :: proc(hir_module: ^hir.Module, symbols: ^symbol.Table, function_name: string) -> (types.Type, bool) {
fn_symbol := symbol.intern(symbols, function_name)
for function in hir_module.functions {
if function.name == fn_symbol {
return function.result, true
}
}
return types.INVALID, false
}
@(test)
float_constraint_resolves_to_f64 :: proc(t: ^testing.T) {
text := `make func() float {
pi float = 3.14
return pi
}
main func() void {
_ = make()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
pi_type, found := float_local_type(&hir_module, &symbols, "make", "pi")
result_type, _ := float_result_type(&hir_module, &symbols, "make")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found)
testing.expect_value(t, pi_type, types.F64)
testing.expect_value(t, result_type, types.F64)
}
@(test)
float_constraint_accepts_integer_literal :: proc(t: ^testing.T) {
text := `make func() float {
pi float = 3
return pi
}
main func() void {
_ = make()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
pi_type, found := float_local_type(&hir_module, &symbols, "make", "pi")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found)
testing.expect_value(t, pi_type, types.F64)
}
@(test)
float_constraint_result_resolves_to_f64 :: proc(t: ^testing.T) {
text := `make func() float {
return 3.0
}
main func() void {
_ = make()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
result_type, found := float_result_type(&hir_module, &symbols, "make")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found)
testing.expect_value(t, result_type, types.F64)
}
@(test)
float_constraint_widens_f32_to_f64 :: proc(t: ^testing.T) {
text := `wide func(a f32, b f64) float {
x float = a
x = b
return x
}
main func() void {
_ = wide(1.0, 2.0)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
x_type, found := float_local_type(&hir_module, &symbols, "wide", "x")
result_type, _ := float_result_type(&hir_module, &symbols, "wide")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found)
testing.expect_value(t, x_type, types.F64)
testing.expect_value(t, result_type, types.F64)
}
@(test)
int_constraint_rejects_float_initializer :: proc(t: ^testing.T) {
text := `main func() void {
x int = 1.0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_constraint_error := false
for diagnostic in diagnostics.items {
found_constraint_error = found_constraint_error ||
strings.contains(diagnostic.message, "could not resolve the 'int' constraint for local 'x'")
}
testing.expect(t, found_constraint_error)
}
@(test)
float_constraint_rejects_runtime_integer :: proc(t: ^testing.T) {
text := `take func(n i32) void {
x float = n
}
main func() void {
take(7)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_convert_error := false
for diagnostic in diagnostics.items {
found_convert_error = found_convert_error ||
strings.contains(diagnostic.message, "cannot implicitly convert i32 to f64")
}
testing.expect(t, found_convert_error)
}
@(test)
range_constraint_local_resolves_to_inferred_range :: proc(t: ^testing.T) {
text := `make func() range {
r range :: 0..10
return r
}
main func() void {
_ = make()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
r_type, found := float_local_type(&hir_module, &symbols, "make", "r")
result_type, _ := float_result_type(&hir_module, &symbols, "make")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found)
testing.expect(t, types.is_range(r_type, &hir_module.types))
testing.expect_value(t, types.child_type(r_type, &hir_module.types), types.I8)
testing.expect(t, types.is_range(result_type, &hir_module.types))
}
@(test)
range_constraint_param_and_result_monomorphize :: proc(t: ^testing.T) {
text := `pass func(r range) range {
return r
}
main func() void {
once :: 0..5
for pass(once) |v| {
_ = v
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
result_type, found := float_result_type(&hir_module, &symbols, "pass")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found)
testing.expect(t, types.is_range(result_type, &hir_module.types))
}
@(test)
int_param_rejects_float_argument :: proc(t: ^testing.T) {
text := `take func(x int) int {
return x
}
main func() void {
_ = take(1.5)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_reject := false
for diagnostic in diagnostics.items {
found_reject = found_reject ||
strings.contains(diagnostic.message, "cannot pass f64 to 'int' parameter 'x'")
}
testing.expect(t, found_reject)
}
@(test)
float_param_accepts_integer_literal_argument :: proc(t: ^testing.T) {
text := `take func(x float) float {
return x
}
main func() void {
y f64 = take(3)
_ = y
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
result_type, found := float_result_type(&hir_module, &symbols, "take")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found)
testing.expect_value(t, result_type, types.F64)
}
@(test)
undefined_accepts_concrete_runtime_annotations :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
y i32
}
main func() void {
point Point = undefined
pointer @i32 = undefined
maybe ?i32 = undefined
_ = point
_ = pointer
_ = maybe
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
main_symbol := symbol.intern(&symbols, "main")
fill_count := 0
for function, function_index in hir_module.functions {
if function.name != main_symbol {
continue
}
for instruction in ir_module.functions[function_index].instructions {
fill_count += 1 if instruction.op == .Fill else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, fill_count, 3)
}
@(test)
undefined_rejects_non_declaration_uses_and_unresolved_inference :: proc(t: ^testing.T) {
text := `global :: undefined
main func() void {
immutable :: undefined
typed_immutable int :: undefined
unresolved int = undefined
existing i32 = 1
existing = undefined
mismatch int = undefined
mismatch = 1
mismatch = 1.0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
immutable_count := 0
found_unresolved := false
found_assignment := false
found_incompatible := false
for diagnostic in diagnostics.items {
immutable_count += 1 if strings.contains(diagnostic.message, "'undefined' requires a mutable local declaration") else 0
found_unresolved = found_unresolved || strings.contains(diagnostic.message, "could not infer a concrete type for local 'unresolved'")
found_assignment = found_assignment || strings.contains(diagnostic.message, "'undefined' is only valid as a mutable declaration initializer")
found_incompatible = found_incompatible || strings.contains(diagnostic.message, "cannot implicitly convert f64 to i8")
}
testing.expect(t, immutable_count >= 2)
testing.expect(t, found_unresolved)
testing.expect(t, found_assignment)
testing.expect(t, found_incompatible)
}
@(test)
compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) {
// A compound assignment to an indexed lvalue must compute the element address
// once and reuse it for the load and the store, rather than re-lowering the
// lvalue (which would re-evaluate any side-effecting index subexpression).
text := `index usize = 1
bump func() usize {
return index
}
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_index usize = 0
column_index usize = 1
row func() usize {
return row_index
}
column func() usize {
return column_index
}
pointer_for func(value @mut i32) @mut i32 {
return value
}
main func() i32 {
matrix [2]mut [2]mut i32 = [[1, 2], [3, 4]]
(matrix[row()])[column()] += 1
boxes [2]mut Box = [Box { value = 5 }, Box { value = 6 }]
boxes[row()].value += 1
value i32 = 7
pointer_for(&value)^ += 1
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
testing.expect_value(t, len(diagnostics.items), 0)
call_count := 0
call_names: [4]string
index_address_count := 0
field_address_count := 0
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction in function.instructions {
#partial switch instruction.op {
case .Call:
function_id := ir.as_function(instruction.target)
if call_count < len(call_names) &&
function_id != ir.INVALID_FUNCTION &&
int(function_id) < len(hir_module.functions) {
call_names[call_count] = symbol.resolve(
&symbols,
hir_module.functions[function_id].name,
)
}
call_count += 1
case .Index_Address: index_address_count += 1
case .Field_Address: field_address_count += 1
case:
}
}
}
// row(), column(), the second row(), and pointer_for() each run once. The
// nested matrix target needs two index addresses; the indexed field needs
// one index address and one field address.
testing.expect_value(t, call_count, 4)
testing.expect_value(t, call_names, [4]string{"row", "column", "row", "pointer_for"})
testing.expect_value(t, index_address_count, 3)
testing.expect_value(t, field_address_count, 1)
}
@(test)
compound_assignment_supports_pointer_add_only :: proc(t: ^testing.T) {
valid_text := `main func() i32 {
values [3]mut i32 = [10, 20, 30]
pointer *mut i32 = (&values).ptr
pointer += 1
offset usize = 1
pointer += offset
return pointer^
}
`
source_file := source.Source{path="test.bro", text=valid_text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
pointer_add_count := 0
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction in function.instructions {
pointer_add_count += 1 if instruction.op == .Pointer_Add else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, pointer_add_count, 2)
invalid_text := `main func() void {
values [1]mut i32 = [10]
pointer *mut i32 = (&values).ptr
pointer -= 1
}
`
invalid_source := source.Source{path="invalid.bro", text=invalid_text}
invalid_diagnostics := source.init_diagnostics(&invalid_source)
defer source.destroy_diagnostics(&invalid_diagnostics)
invalid_symbols := symbol.init_table()
defer symbol.destroy_table(&invalid_symbols)
invalid_stream := lexer.lex(&invalid_source, &invalid_diagnostics, &invalid_symbols)
defer delete(invalid_stream.items)
invalid_ast := parser.parse(&invalid_stream, &invalid_source, &invalid_diagnostics)
defer ast.destroy_module(&invalid_ast)
invalid_hir := checker.check(&invalid_ast, &invalid_diagnostics, &invalid_symbols)
defer hir.destroy_module(&invalid_hir)
found := false
for diagnostic in invalid_diagnostics.items {
found = found || strings.contains(
diagnostic.message,
"many-item pointers only support '+=' compound assignment",
)
}
testing.expect(t, found)
}
@(test)
compound_assignment_preserves_checked_numeric_operations :: proc(t: ^testing.T) {
text := `main func() i32 {
signed i32 = 24
signed += 6
signed -= 2
signed *= 3
signed = divtrunc!(signed, 4)
unsigned u32 = 24
unsigned += 6
unsigned -= 2
unsigned *= 3
unsigned = divtrunc!(unsigned, 4)
real f64 = 24.0
real += 6.0
real -= 2.0
real *= 3.0
real /= 4.0
return signed
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
operation_counts: [hir.Assignment_Op]int
for statement_id in hir_module.functions[0].body {
statement := hir_module.statements[statement_id]
if statement.kind == .Assignment {
operation_counts[statement.assignment_op] += 1
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, operation_counts[.Add], 3)
testing.expect_value(t, operation_counts[.Sub], 3)
testing.expect_value(t, operation_counts[.Mul], 3)
testing.expect_value(t, operation_counts[.Div], 1)
add_count := 0
sub_count := 0
mul_count := 0
div_count := 0
div_trunc_count := 0
for instruction in ir_module.functions[0].instructions {
#partial switch instruction.op {
case .Add_Checked: add_count += 1
case .Sub_Checked: sub_count += 1
case .Mul_Checked: mul_count += 1
case .Div_Checked: div_count += 1
case .Div_Trunc_Checked: div_trunc_count += 1
case:
}
}
testing.expect_value(t, add_count, 3)
testing.expect_value(t, sub_count, 3)
testing.expect_value(t, mul_count, 3)
testing.expect_value(t, div_count, 1)
testing.expect_value(t, div_trunc_count, 2)
}
@(test)
compound_assignment_rejects_narrowing_and_mixed_numeric_families :: proc(t: ^testing.T) {
text := `main func() void {
narrow i8 = 1
wide i32 = 2
narrow += wide
signed i32 = 3
unsigned u32 = 4
signed += unsigned
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_narrowing := false
found_mixed_family := false
for diagnostic in diagnostics.items {
found_narrowing = found_narrowing ||
strings.contains(diagnostic.message, "cannot implicitly convert i32 to i8")
found_mixed_family = found_mixed_family ||
strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
}
testing.expect(t, found_narrowing)
testing.expect(t, found_mixed_family)
}
@(test)
compound_assignment_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-compound"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/compound_assignment", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 23)
}
@(test)
binary_arithmetic_rejects_non_numeric_operands :: proc(t: ^testing.T) {
text := `main func() i32 {
a i32 = 1
b u32 = 2
_ = a + b
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
}
testing.expect(t, found)
}
@(test)
compound_assignment_requires_writable_target :: proc(t: ^testing.T) {
text := `main func() i32 {
x :: 5
x += 1
return x
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "assignment target is not writable")
}
testing.expect(t, found)
}
@(test)
checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) {
text := `main func() i32 {
a i32 = 10
b i32 = 3
c i32 = a - b
return divtrunc!(c, b)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i32"))
testing.expect(t, strings.contains(llvm_text, "sdiv i32"))
testing.expect(t, strings.contains(llvm_text, "divzero_trap"))
testing.expect(t, strings.contains(llvm_text, "divovf_trap"))
}
@(test)
integer_slash_is_rejected_and_float_slash_remains_available :: proc(t: ^testing.T) {
Case :: struct {text, want: string}
invalid := []Case{
{text=`main func() void {
a i32 = 4
b i32 = 2
_ = a / b
}`, want="integer '/' is not allowed"},
{text=`main func() void {
a u32 = 4
b u32 = 2
_ = a / b
}`, want="integer '/' is not allowed"},
{text=`main func() void {
_ = 4 / 2
}`, want="integer '/' is not allowed"},
{text=`main func() void {
values [4 / 2]u8 = undefined
_ = &values
}`, want="integer '/' is not allowed"},
{text=`half func($value i32) i32 { return value / 2 }
main func() void { _ = $half(4) }`, want="integer '/' is not allowed"},
{text=`main func() void {
value i32 = 8
value /= 2
}`, want="assign through an explicit division builtin"},
}
for test_case in invalid {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.want)
}
testing.expect(t, found)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
text := `main func() void {
value f32 = 5.0 / 2.0
value /= 2.0
_ = value
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
division_builtins_diagnose_arity_operands_and_comptime_failures :: proc(t: ^testing.T) {
text := `bad_arity :: divfloor!(1)
bad_bool :: rem!(true, false)
bad_family :: mod!(i32(5), f32(3))
zero_trunc :: divtrunc!(1, 0)
zero_floor :: divfloor!(1.0, 0.0)
zero_exact :: divexact!(1, 0)
zero_ceil :: divceil!(1.0, 0.0)
zero_rem :: rem!(1, 0)
zero_mod :: mod!(1.0, 0.0)
inexact :: divexact!(5, 3)
overflow_trunc :: divtrunc!(minval!(i32), -1)
overflow_floor :: divfloor!(minval!(i32), -1)
overflow_exact :: divexact!(minval!(i32), -1)
overflow_ceil :: divceil!(minval!(i32), -1)
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
zero_count := 0
found_arity, found_operands, found_exact := false, false, false
overflow_count := 0
for diagnostic in diagnostics.items {
found_arity = found_arity || strings.contains(diagnostic.message, "expects 2 arguments")
found_operands = found_operands || strings.contains(diagnostic.message, "compatible numeric operands")
found_exact = found_exact || strings.contains(diagnostic.message, "exact division has a remainder")
if strings.contains(diagnostic.message, "signed integer division overflow") {
overflow_count += 1
}
if strings.contains(diagnostic.message, "division builtin denominator is zero") {
zero_count += 1
}
}
testing.expect(t, found_arity)
testing.expect(t, found_operands)
testing.expect(t, found_exact)
testing.expect_value(t, overflow_count, 4)
testing.expect_value(t, zero_count, 6)
}
@(test)
division_family_compiles_and_runs_for_integer_and_float_scalars :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-division-family"
main_path := "/tmp/brolang-test-division-family/main.bro"
output := "/tmp/brolang-test-division-family-output"
text := `COUNT :: divexact!(8, 2)
items [divceil!(10, 3)]u8 :: [0, 0, 0, 0]
OPEN :: 5
open_ceil i32 :: divceil!(OPEN, 3)
check_i32 func(a, b, qt, qf, qc, r, m i32) bool {
return divtrunc!(a, b) == qt and divfloor!(a, b) == qf and
divceil!(a, b) == qc and rem!(a, b) == r and mod!(a, b) == m
}
check_f32 func(a, b, qt, qf, qc, r, m f32) bool {
return divtrunc!(a, b) == qt and divfloor!(a, b) == qf and
divceil!(a, b) == qc and rem!(a, b) == r and mod!(a, b) == m
}
check_f64 func(a, b, qt, qf, qc, r, m f64) bool {
return divtrunc!(a, b) == qt and divfloor!(a, b) == qf and
divceil!(a, b) == qc and rem!(a, b) == r and mod!(a, b) == m
}
edge_rem func(a, b i32) i32 { return rem!(a, b) }
edge_mod func(a, b i32) i32 { return mod!(a, b) }
main func() i32 {
if COUNT != 4 or items.len != 4 or open_ceil != 2 { return 1 }
if !check_i32(5, 3, 1, 1, 2, 2, 2) { return 2 }
if !check_i32(5, -3, -1, -2, -1, 2, -1) { return 3 }
if !check_i32(-5, 3, -1, -2, -1, -2, 1) { return 4 }
if !check_i32(-5, -3, 1, 1, 2, -2, -2) { return 5 }
if divtrunc!(u32(5), u32(3)) != 1 or divfloor!(u32(5), u32(3)) != 1 or
divceil!(u32(5), u32(3)) != 2 or rem!(u32(5), u32(3)) != 2 or mod!(u32(5), u32(3)) != 2 { return 6 }
if divexact!(i32(6), i32(3)) != 2 or divexact!(u32(6), u32(3)) != 2 { return 7 }
if !check_f32(f32(5.0), f32(3.0), f32(1.0), f32(1.0), f32(2.0), f32(2.0), f32(2.0)) or
!check_f32(f32(5.0), f32(-3.0), f32(-1.0), f32(-2.0), f32(-1.0), f32(2.0), f32(-1.0)) or
!check_f32(f32(-5.0), f32(3.0), f32(-1.0), f32(-2.0), f32(-1.0), f32(-2.0), f32(1.0)) or
!check_f32(f32(-5.0), f32(-3.0), f32(1.0), f32(1.0), f32(2.0), f32(-2.0), f32(-2.0)) { return 8 }
if !check_f64(5.0, 3.0, 1.0, 1.0, 2.0, 2.0, 2.0) or
!check_f64(5.0, -3.0, -1.0, -2.0, -1.0, 2.0, -1.0) or
!check_f64(-5.0, 3.0, -1.0, -2.0, -1.0, -2.0, 1.0) or
!check_f64(-5.0, -3.0, 1.0, 1.0, 2.0, -2.0, -2.0) { return 9 }
if divexact!(f32(6.0), f32(3.0)) != 2.0 or divexact!(f64(6.0), f64(3.0)) != 2.0 { return 10 }
if edge_rem(-2147483648, -1) != 0 or edge_mod(-2147483648, -1) != 0 { return 11 }
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
division_builtins_trap_for_runtime_zero_overflow_and_inexact_results :: proc(t: ^testing.T) {
Case :: struct {
name: string,
type_name: string,
left: string,
right: string,
}
cases := [?]Case{
{name="divtrunc", type_name="i32", left="1", right="0"},
{name="divfloor", type_name="f32", left="f32(1.0)", right="f32(0.0)"},
{name="divexact", type_name="f64", left="1.0", right="0.0"},
{name="divceil", type_name="i32", left="1", right="0"},
{name="rem", type_name="f32", left="f32(1.0)", right="f32(0.0)"},
{name="mod", type_name="f64", left="1.0", right="0.0"},
{name="divexact", type_name="i32", left="5", right="3"},
{name="divtrunc", type_name="i32", left="-2147483648", right="-1"},
{name="divfloor", type_name="i32", left="-2147483648", right="-1"},
{name="divexact", type_name="i32", left="-2147483648", right="-1"},
{name="divceil", type_name="i32", left="-2147483648", right="-1"},
}
for test_case, index in cases {
directory := fmt.aprintf("/tmp/brolang-test-division-trap-%d", index)
main_path := fmt.aprintf("%s/main.bro", directory)
output := fmt.aprintf("/tmp/brolang-test-division-trap-output-%d", index)
text := fmt.aprintf(
"invoke func(a, b %s) %s {{ return %s!(a, b) }}\nmain func() void {{ _ = invoke(%s, %s) }}\n",
test_case.type_name, test_case.type_name, test_case.name, test_case.left, test_case.right,
)
_ = os2.remove_all(directory)
_ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect(t, !state.success)
_ = os.remove(output)
_ = os2.remove_all(directory)
delete(text)
delete(output)
delete(main_path)
delete(directory)
}
}
@(test)
qualified_division_builtin_names_resolve_as_package_functions :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-qualified-division"
math_directory := "/tmp/brolang-test-qualified-division/math"
app_directory := "/tmp/brolang-test-qualified-division/app"
math_path := "/tmp/brolang-test-qualified-division/math/math.bro"
main_path := "/tmp/brolang-test-qualified-division/app/main.bro"
output := "/tmp/brolang-test-qualified-division-output"
math_text := `divfloor func(a, b i32) i32 { return a + b }
`
main_text := `math :: import "../math"
main func() i32 { return math.divfloor(20, 22) }
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.make_directory(math_directory) == nil)
testing.expect(t, os.make_directory(app_directory) == nil)
testing.expect(t, os.write_entire_file(math_path, transmute([]byte)math_text))
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)main_text))
status := compiler_core.compile_package(app_directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
division_builtins_emit_guards_rounding_and_single_integer_divisions :: proc(t: ^testing.T) {
text := `floor_i32 func(a, b i32) i32 { return divfloor!(a, b) }
ceil_i32 func(a, b i32) i32 { return divceil!(a, b) }
exact_i32 func(a, b i32) i32 { return divexact!(a, b) }
floor_u32 func(a, b u32) u32 { return divfloor!(a, b) }
rem_i16 func(a, b i16) i16 { return rem!(a, b) }
mod_i16 func(a, b i16) i16 { return mod!(a, b) }
floor_f32 func(a, b f32) f32 { return divfloor!(a, b) }
ceil_f64 func(a, b f64) f64 { return divceil!(a, b) }
exact_f32 func(a, b f32) f32 { return divexact!(a, b) }
rem_f64 func(a, b f64) f64 { return rem!(a, b) }
mod_f32 func(a, b f32) f32 { return mod!(a, b) }
main func() void {
_ = floor_i32(5, 3)
_ = ceil_i32(5, 3)
_ = exact_i32(6, 3)
_ = floor_u32(5, 3)
_ = rem_i16(5, 3)
_ = mod_i16(5, 3)
_ = floor_f32(f32(5.0), f32(3.0))
_ = ceil_f64(5.0, 3.0)
_ = exact_f32(f32(6.0), f32(3.0))
_ = rem_f64(5.0, 3.0)
_ = mod_f32(f32(5.0), f32(3.0))
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, strings.count(llvm_text, "sdiv i32"), 3)
testing.expect(t, !strings.contains(llvm_text, "srem i32"))
testing.expect(t, strings.contains(llvm_text, "udiv i32"))
testing.expect(t, strings.contains(llvm_text, "srem i16"))
testing.expect(t, strings.contains(llvm_text, "remspecial"))
testing.expect(t, strings.contains(llvm_text, "divzero_trap"))
testing.expect(t, strings.contains(llvm_text, "divovf_trap"))
testing.expect(t, strings.contains(llvm_text, "call float @llvm.floor.f32"))
testing.expect(t, strings.contains(llvm_text, "call double @llvm.ceil.f64"))
testing.expect(t, strings.contains(llvm_text, "call float @llvm.trunc.f32"))
testing.expect(t, strings.contains(llvm_text, "frem double"))
}
@(test)
distinct_types_preserve_nominal_identity_and_backing_representation :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
y i32
}
UserID :: distinct u32
OtherID :: distinct u32
PointID :: distinct Point
Bytes :: distinct [2]u8
WrappedID :: distinct UserID
Flag :: distinct bool
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)
sum UserID = id + 1
matches bool = id == 7
raw u32 = u32(id)
wide usize = usize(id)
real f64 = f64(id)
inner UserID = UserID(wrapped)
terminal u32 = u32(wrapped)
flag_matches bool = Flag(true) == Flag(true)
_ = maybe
_ = pointer
_ = point
_ = bytes
_ = wrapped
_ = sum
_ = matches
_ = raw
_ = wide
_ = real
_ = inner
_ = terminal
_ = flag_matches
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"))
nominal_hir_add := false
for hir_expr in hir_module.exprs {
nominal_hir_add = nominal_hir_add || hir_expr.kind == .Add && hir_expr.type == user_id
}
testing.expect(t, nominal_hir_add)
retype_count := 0
nominal_ir_add := false
u32_cast, usize_cast, f64_cast := false, false, false
for function in ir_module.functions {
for instruction in function.instructions {
retype_count += 1 if instruction.op == .Retype else 0
nominal_ir_add = nominal_ir_add || instruction.op == .Add_Checked && instruction.type == user_id
if instruction.op == .Scalar_Cast {
u32_cast = u32_cast || instruction.type == types.U32
usize_cast = usize_cast || instruction.type == types.USIZE
f64_cast = f64_cast || instruction.type == types.F64
}
}
}
testing.expect(t, nominal_ir_add)
testing.expect(t, u32_cast && usize_cast && f64_cast)
testing.expect(t, retype_count >= 5)
}
@(test)
distinct_construction_casts_to_scalar_backing_and_infers_bound_global :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-distinct-construction"
main_path := "/tmp/brolang-test-distinct-construction/main.bro"
output := "/tmp/brolang-test-distinct-construction-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os2.make_directory_all(directory) == nil)
text := `UserID :: distinct u32
OtherID :: distinct u32
NO_ID :: maxval!(UserID)
Data :: union { id UserID }
Record :: struct { data Data = Data{ id = NO_ID } }
main func() i32 {
small u8 = 7
wide usize = 8
a UserID = UserID(small)
b UserID = UserID(wide)
c UserID = UserID(OtherID(9))
d UserID :: $UserID(usize(10))
record Record = {}
_ = record
if u32(a) != 7 or u32(b) != 8 or u32(c) != 9 or u32(d) != 10 { return 1 }
return 0
}
`
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
distinct_types_reject_implicit_conversions_and_invalid_backings :: proc(t: ^testing.T) {
text := `Opaque :: opaque
UserID :: distinct u32
OtherID :: distinct u32
BadInt :: distinct int
BadVoid :: distinct void
BadFunction :: distinct c_func() void
BadOpaque :: distinct Opaque
foreign c_func(value UserID) void
foreign_pointer c_func(value @UserID) void
main func() void {
raw u32 = 1
id UserID = raw
backing u32 = UserID(2)
other OtherID = UserID(3)
_ = UserID()
_ = UserID(1, 2)
left UserID :: UserID(5)
raw_operand u32 :: u32(6)
other_operand OtherID :: OtherID(6)
_ = left + raw_operand
_ = raw_operand + left
_ = left + other_operand
_ = left == raw_operand
_ = raw_operand == left
_ = left == other_operand
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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_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_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_arity)
testing.expect(t, found_arithmetic)
testing.expect(t, found_comparison)
testing.expect_value(t, foreign_signature_count, 2)
}
@(test)
type_and_function_names_cannot_shadow :: proc(t: ^testing.T) {
text := `Value :: distinct u32
Value func(value i32) i32 {
return value
}
main func() i32 {
return Value(42)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "function 'Value' shadows visible type")
}
testing.expect(t, found)
}
@(test)
distinct_types_compile_and_run_across_packages :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-distinct-types"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/distinct_types", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
native_enums_preserve_identity_members_and_integer_representation :: proc(t: ^testing.T) {
text := `Animal :: enum {
dog
cat
bird
}
Nat :: enum(u16) {
one = 1
two
five = 5
}
global Animal :: Animal.dog
take func(value Animal) Animal {
return value
}
identity c_func(value Nat) Nat {
return value
}
variadic c_func(marker c_int, ...) c_int
main func() i32 {
value Animal = .cat
values [2]Animal :: [.dog, Animal.bird]
number Nat = identity(.two)
ordinal c_int :: c_int(number)
_ = variadic(0, number)
if take(value) == Animal.cat and values[0] != values[1] and number == Nat.two and ordinal == 2 {
return 0
}
return 1
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
animal := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Animal")))
nat := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Nat")))
animal_node, animal_ok := types.node(&ast_module.type_store, animal)
nat_node, nat_ok := types.node(&ast_module.type_store, nat)
animal_members := types.enum_members_for(&ast_module.type_store, animal)
nat_members := types.enum_members_for(&ast_module.type_store, nat)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, animal_ok && nat_ok)
testing.expect(t, types.is_enum(animal, &ast_module.type_store))
testing.expect_value(t, animal_node.child, types.U16)
testing.expect(t, !animal_node.explicit_backing)
testing.expect_value(t, nat_node.child, types.U16)
testing.expect(t, nat_node.explicit_backing)
testing.expect_value(t, len(animal_members), 3)
testing.expect_value(t, animal_members[0].value, i128(1))
testing.expect_value(t, animal_members[2].value, i128(3))
testing.expect_value(t, nat_members[0].value, i128(1))
testing.expect_value(t, nat_members[1].value, i128(2))
testing.expect_value(t, nat_members[2].value, i128(5))
testing.expect(t, strings.contains(llvm_text, "zext i16"))
testing.expect_value(t, types.runtime_representation(animal, &ast_module.type_store), types.U16)
testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(2))
testing.expect(t, hir_module.globals[0].is_static)
testing.expect_value(t, hir_module.globals[0].static_value, i64(1))
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i16 1"))
found_promotion := false
for function in ir_module.functions {
for instruction in function.instructions {
found_promotion = found_promotion || instruction.op == .C_Vararg_Promote
}
}
testing.expect(t, found_promotion)
}
@(test)
keywords_are_valid_enum_members_and_tagged_union_variants :: proc(t: ^testing.T) {
testing.expect(t, token.is_keyword(.Keyword_Func))
testing.expect(t, token.is_keyword(.Keyword_Hide))
testing.expect(t, token.is_keyword(.Keyword_C_Longdouble))
testing.expect(t, !token.is_keyword(.Identifier))
testing.expect(t, !token.is_keyword(.Underscore))
text := `TokenKind :: enum {
if
else
return
}
Token :: union(TokenKind) {
if i32
else void
return i32
}
kind func(value bool) TokenKind {
if value {
return .if
}
return TokenKind.else
}
main func() i32 {
first TokenKind = kind(true)
second TokenKind = .return
a Token = Token{ if = 1 }
b Token = Token{ else }
c Token = .return{2}
total i32 = a.if + c.return
match first {
.if: total = total + 1
.else: total = total + 2
.return: total = total + 3
}
match b {
.if |value|: total = total + value
.else: total = total + 4
.return |value|: total = total + value
}
_ = second
return total
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, len(llvm_text) > 0)
}
@(test)
keyword_names_remain_invalid_for_struct_fields :: proc(t: ^testing.T) {
text := `Bad :: struct {
if i32
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "expected a struct field name")
}
testing.expect(t, found)
}
@(test)
unbacked_enum_uses_global_u16_backing :: proc(t: ^testing.T) {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
strings.write_string(&builder, "Large :: enum {\n")
for index in 0..<257 {
fmt.sbprintf(&builder, "value_%d\n", index)
}
strings.write_string(&builder, "}\nmain func() void {}\n")
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
large := types.find_named(&module.type_store, 0, u32(symbol.intern(&symbols, "Large")))
item, ok := types.node(&module.type_store, large)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, ok)
testing.expect_value(t, item.child, types.U16)
testing.expect_value(t, len(types.enum_members_for(&module.type_store, large)), 257)
}
@(test)
native_enum_invalid_declarations_and_operations_are_diagnosed :: proc(t: ^testing.T) {
text := `Empty :: enum {}
Dense :: enum {
zero = 0
one
}
BadBacking :: enum(f32) {
value
}
Duplicate :: enum(u8) {
value
value
}
Jumbled :: enum(i8) {
second = 2
first = 1
}
Overflow :: enum(u8) {
value = 256
}
Other :: enum {
value
}
foreign c_func(value Dense) void
allowed c_func(value Overflow) Overflow
main func() void {
dense Dense = Other.value
_ = Dense.zero + Dense.one
_ = Dense.zero < Dense.one
_ = Dense.missing
_ = .zero
_ = dense
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_empty := false
found_unbacked_value := false
found_backing := false
found_duplicate := false
found_order := false
found_overflow := false
found_foreign := false
found_conversion := false
found_arithmetic := false
found_comparison := false
found_member := false
found_context := false
for diagnostic in diagnostics.items {
found_empty = found_empty || strings.contains(diagnostic.message, "require at least one member")
found_unbacked_value = found_unbacked_value || strings.contains(diagnostic.message, "explicit enum values require a backing type")
found_backing = found_backing || strings.contains(diagnostic.message, "requires a concrete integer backing type")
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate enum member")
found_order = found_order || strings.contains(diagnostic.message, "strictly increasing")
found_overflow = found_overflow || strings.contains(diagnostic.message, "does not fit in u8")
found_foreign = found_foreign || strings.contains(diagnostic.message, "requires concrete parameter types")
found_conversion = found_conversion || strings.contains(diagnostic.message, "cannot implicitly convert")
found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
found_comparison = found_comparison || strings.contains(diagnostic.message, "enum values only support")
found_member = found_member || strings.contains(diagnostic.message, "unknown enum member")
found_context = found_context || strings.contains(diagnostic.message, "requires an enum context")
}
testing.expect(t, found_empty)
testing.expect(t, found_unbacked_value)
testing.expect(t, found_backing)
testing.expect(t, found_duplicate)
testing.expect(t, found_order)
testing.expect(t, found_overflow)
testing.expect(t, found_foreign)
testing.expect(t, found_conversion)
testing.expect(t, found_arithmetic)
testing.expect(t, found_comparison)
testing.expect(t, found_member)
testing.expect(t, found_context)
}
@(test)
native_enums_compile_and_run_across_packages :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-enums"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/enums", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
translate_c_emits_native_bindings_and_round_trips :: proc(t: ^testing.T) {
result := cimport.init_result(context.allocator)
// type table: [0]=c_int, [1]=c_ulong, [2]=Record(Pair),
// [3]=Function(c_int)->c_int, [4]=Pointer->Function (callback),
// [5]=void, [6]=Pointer->void
append(&result.types, cimport.Type{kind = .C_Int, child = cimport.INVALID_TYPE})
append(&result.types, cimport.Type{kind = .C_Ulong, child = cimport.INVALID_TYPE})
append(&result.types, cimport.Type{kind = .Record, record = 0, child = cimport.INVALID_TYPE})
func_params := []cimport.Type_Id{cimport.Type_Id(0)}
append(&result.types, cimport.Type{kind = .Function, params = func_params, child = cimport.Type_Id(0)})
append(&result.types, cimport.Type{kind = .Pointer, child = cimport.Type_Id(3)})
append(&result.types, cimport.Type{kind = .Void, child = cimport.INVALID_TYPE})
append(&result.types, cimport.Type{kind = .Pointer, child = cimport.Type_Id(5), mutable = true})
// record 0: Pair { left c_int; right c_int }
pair_fields: [dynamic]cimport.Field
append(&pair_fields, cimport.Field{name = "left", type = cimport.Type_Id(0)})
append(&pair_fields, cimport.Field{name = "right", type = cimport.Type_Id(0)})
append(&result.records, cimport.Record{name = "Pair", fields = pair_fields, kind = .Struct, complete = true})
// record 1: union Choice -> commented (no native spelling)
choice_fields: [dynamic]cimport.Field
append(&choice_fields, cimport.Field{name = "tag", type = cimport.Type_Id(0)})
append(&result.records, cimport.Record{name = "Choice", fields = choice_fields, kind = .Union, complete = true})
// record 2: incomplete struct -> opaque
append(&result.records, cimport.Record{name = "Handle", kind = .Struct, complete = false})
// typedef aliases: a scalar, a callback function pointer, and a C void pointer
append(&result.aliases, cimport.Alias{name = "Size", type = cimport.Type_Id(1)})
append(&result.aliases, cimport.Alias{name = "Mapper", type = cimport.Type_Id(4)})
append(&result.aliases, cimport.Alias{name = "RawPtr", type = cimport.Type_Id(6)})
add_params := []cimport.Type_Id{cimport.Type_Id(0), cimport.Type_Id(0)}
add_param_names := []string{"a", "b"}
append(&result.functions, cimport.Function{name = "imported_add", params = add_params, param_names = add_param_names, result = cimport.Type_Id(0)})
raw_params := []cimport.Type_Id{cimport.Type_Id(6)}
raw_param_names := []string{"ptr"}
append(&result.functions, cimport.Function{name = "consume_raw", params = raw_params, param_names = raw_param_names, result = cimport.Type_Id(5)})
append(&result.macros, cimport.Macro_Constant{
name = "MAX_LEN",
type = cimport.Type_Id(0),
value = {kind = .Integer, type = cimport.Type_Id(0), integer = 256},
})
// external variable -> commented (no native spelling)
append(&result.variables, cimport.Variable{name = "some_global", type = cimport.Type_Id(0), mutable = true})
result.available = true
output := translatec.emit(&result, "test.h")
defer delete(output)
defer {
delete(result.types)
delete(result.records)
delete(result.aliases)
delete(result.functions)
delete(result.variables)
delete(result.macros)
delete(pair_fields)
delete(choice_fields)
}
testing.expect(t, strings.contains(output, "Pair :: c_struct {"))
testing.expect(t, strings.contains(output, "\tleft c_int"))
testing.expect(t, strings.contains(output, "\tright c_int"))
testing.expect(t, strings.contains(output, "Size :: alias c_ulong"))
// Function-pointer types carry no parameter names, so the callback renders `_`.
testing.expect(t, strings.contains(output, "Mapper :: alias ?*c_func(_ c_int) c_int"))
testing.expect(t, strings.contains(output, "RawPtr :: alias ?*mut anyopaque"))
testing.expect(t, strings.contains(output, "Handle :: opaque"))
// Real C parameter names are used when present.
testing.expect(t, strings.contains(output, "imported_add c_func(a c_int, b c_int) c_int"))
testing.expect(t, strings.contains(output, "consume_raw c_func(ptr ?*mut anyopaque) void"))
testing.expect(t, strings.contains(output, "MAX_LEN c_int :: 256"))
testing.expect(t, strings.contains(output, "# unsupported in bindings: C union 'Choice'"))
testing.expect(t, strings.contains(output, "# unsupported in bindings: external variable 'some_global'"))
// Round-trip: the emitted source must lex + parse with zero diagnostics.
// This guards render_type against drift from loader.translate_c_type and
// exercises the new `alias` declaration syntax.
source_file := source.Source{path = "bindings.bro", text = output}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
translate_c_package_deduplicates_compatible_declarations :: proc(t: ^testing.T) {
first := cimport.init_result(context.allocator)
second := cimport.init_result(context.allocator)
defer {
delete(first.types)
delete(first.records[0].fields)
delete(first.records)
delete(first.functions)
delete(first.macros)
delete(second.types)
delete(second.records[0].fields)
delete(second.records)
delete(second.functions)
delete(second.macros)
}
append(&first.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
append(&first.types, cimport.Type{kind=.Record, record=0, child=cimport.INVALID_TYPE})
append(&first.types, cimport.Type{kind=.Pointer, child=1, mutable=true})
append(&second.types, ..first.types[:])
first_fields: [dynamic]cimport.Field
second_fields: [dynamic]cimport.Field
append(&first_fields, cimport.Field{name="value", type=0})
append(&second_fields, cimport.Field{name="value", type=0})
append(&first.records, cimport.Record{
identity="shared-anonymous-record",
kind=.Struct,
complete=true,
fields=first_fields,
})
append(&second.records, cimport.Record{
identity="shared-anonymous-record",
kind=.Struct,
complete=true,
fields=second_fields,
})
first_params := [?]cimport.Type_Id{0}
second_params := [?]cimport.Type_Id{0}
use_record_params := [?]cimport.Type_Id{2}
first_names := [?]string{"left"}
second_names := [?]string{"right"}
append(&first.functions, cimport.Function{
name="shared",
params=first_params[:],
param_names=first_names[:],
result=0,
})
append(&second.functions, cimport.Function{
name="shared",
params=second_params[:],
param_names=second_names[:],
result=0,
})
append(&second.functions, cimport.Function{
name="use_record",
params=use_record_params[:],
result=0,
})
append(&first.macros, cimport.Macro_Constant{
name="SHARED_VALUE",
type=0,
value={kind=.Integer, type=0, integer=7},
})
append(&second.macros, first.macros[0])
inputs := [?]translatec.Package_Input{
{result=&first, header="first.h", name="first.bro"},
{result=&second, header="second.h", name="second.bro"},
}
outputs, generation_error := translatec.emit_package(inputs[:])
defer translatec.destroy_package_outputs(outputs)
defer delete(generation_error)
testing.expect_value(t, generation_error, "")
testing.expect_value(t, len(outputs), 2)
if len(outputs) == 2 {
combined := fmt.tprintf("%s%s", outputs[0].source, outputs[1].source)
testing.expect_value(t, count_substring_occurrences(combined, "shared c_func("), 1)
testing.expect_value(t, count_substring_occurrences(combined, "SHARED_VALUE c_int :: 7"), 1)
testing.expect(t, strings.contains(outputs[0].source, "__c_first_bro_record_0 :: c_struct"))
testing.expect(t, strings.contains(outputs[1].source, "use_record c_func(_ ?*mut __c_first_bro_record_0) c_int"))
}
second.macros[0].value.integer = 8
conflicting, conflict_error := translatec.emit_package(inputs[:])
defer translatec.destroy_package_outputs(conflicting)
defer delete(conflict_error)
testing.expect_value(t, len(conflicting), 0)
testing.expect(t, strings.contains(conflict_error, "conflicting generated C declaration 'SHARED_VALUE'"))
}
@(test)
translate_c_package_cli_requires_unambiguous_outputs :: proc(t: ^testing.T) {
testing.expect_value(t, run_translate_c([]string{
"brolang", "--translate-c", "first.h", "second.h",
}), 2)
testing.expect_value(t, run_translate_c([]string{
"brolang", "--translate-c", "a/same.h", "b/same.h", "--output-dir", "/tmp/unused",
}), 2)
}
@(test)
sink_named_parameters_are_allowed_and_not_duplicates :: proc(t: ^testing.T) {
// Generated bindings use `_` for unnamed C params; the parser must accept it and
// the checker must not flag repeated `_` as duplicate parameters.
text := `foo c_func(_ c_int, _ c_int) c_int
main func() void {
_ = foo(1, 2)
}
`
source_file := source.Source{path = "test.bro", text = text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
for diagnostic in diagnostics.items {
testing.expect(t, !strings.contains(diagnostic.message, "duplicate parameter"))
testing.expect(t, !strings.contains(diagnostic.message, "expected parameter name"))
}
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
contextual_inference_resolves_signed_const_chain :: proc(t: ^testing.T) {
text := `X :: 1000
Y int :: X
Z i32 :: Y
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
// The concrete i32 on Z flows backward through Y to the open constant X, so all
// three resolve to i32 instead of X/Y staying at the literal's smallest signed type.
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, types.equal(hir_module.globals[0].type, types.I32))
testing.expect(t, types.equal(hir_module.globals[1].type, types.I32))
testing.expect(t, types.equal(hir_module.globals[2].type, types.I32))
}
@(test)
contextual_inference_open_constants_adopt_unsigned_demand :: proc(t: ^testing.T) {
text := `A :: 10
B u16 :: A
P :: 10
R u32 :: P
N :: 42
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
// An open constant is sign-agnostic until used: it adopts the unsigned family a use
// demands (the literal's signed default would block this). Unconstrained N defaults.
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, types.equal(hir_module.globals[0].type, types.U16)) // A
testing.expect(t, types.equal(hir_module.globals[1].type, types.U16)) // B
testing.expect(t, types.equal(hir_module.globals[2].type, types.U32)) // P
testing.expect(t, types.equal(hir_module.globals[3].type, types.U32)) // R
testing.expect(t, types.equal(hir_module.globals[4].type, types.I8)) // N
}
@(test)
contextual_inference_rejects_constant_that_does_not_fit_demand :: proc(t: ^testing.T) {
text := `BIG :: 100000
C u8 :: BIG
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
// 100000 does not fit u8, so the demand is rejected, BIG defaults to i32, and the
// genuine mismatch surfaces at the use's boundary coercion.
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
}
testing.expect(t, found)
}
@(test)
contextual_inference_does_not_cross_call_boundaries :: proc(t: ^testing.T) {
text := `echo func(p int) int { return p }
A :: 10
R u32 :: echo(A)
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
// The u32 demand on R must not flow through echo into A (that is L3, deferred). A
// stays at its default i8, so the call result fails to coerce to u32.
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i8 to u32")
}
testing.expect(t, found)
}
@(test)
contextual_inference_resolves_locals_like_globals :: proc(t: ^testing.T) {
text := `take_u16 func(_ u16) void {}
get func() u16 {
c :: 10
return c
}
main func() void {
x :: 1000
y int :: x
z i32 :: y
a :: 10
b u16 :: a
n :: 5
take_u16(n)
_ = z
_ = b
_ = get()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
// The same backward propagation works for locals: a constant local adopts the
// unsigned/wider type a later use demands (declaration, call argument, or return),
// so none of these need an explicit annotation. Without it, i8->u16/u32 would error.
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
contextual_inference_demand_from_function_body_reaches_global :: proc(t: ^testing.T) {
text := `take_u16 func(_ u16) void {}
G :: 10
main func() void {
take_u16(G)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
// A demand originating inside a function body (passing G to a u16 parameter) flows
// back to the open-constant global G, resolving it to u16.
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, types.equal(hir_module.globals[0].type, types.U16))
}
@(test)
contextual_inference_flows_through_compound_assignment :: proc(t: ^testing.T) {
text := `main func() void {
s :: 5
v u16 = 0
v += s
_ = v
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
// `s` is used only as the RHS of `v += s`. The assignment target's type (u16) is
// demanded backward onto `s`, resolving the open constant; without it the compound
// assignment would report "arithmetic requires compatible numeric operands".
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
contextual_inference_resolves_open_global_arithmetic_across_uses :: proc(t: ^testing.T) {
text := `take_ci func(_ c_int) void {}
W :: 800
Z :: 40
STEP :: 5
main func() void {
take_ci(W)
x int = W - Z
take_ci(Z)
x += STEP
take_ci(x)
_ = x
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
// `W - Z` mixes two open-constant globals while `Z`'s c_int use comes only after the
// arithmetic, and `STEP` is used only in a compound assignment. With deferred defaulting
// an undemanded open global stays typeless during the fixpoint instead of leaking a
// provisional i16 default, so W/Z/STEP all resolve to c_int. Previously this reported
// "arithmetic requires compatible numeric operands" / "cannot implicitly convert i16 to c_int".
testing.expect_value(t, len(diagnostics.items), 0)
for global in hir_module.globals {
name := symbol.resolve(&symbols, global.name)
if name == "W" || name == "Z" || name == "STEP" {
testing.expect(t, types.equal(global.type, types.C_INT))
}
}
}
@(test)
contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testing.T) {
text := `main func() void {
big :: 100000
c u8 :: big
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
// 100000 does not fit u8, so big keeps its i32 default and the use errors.
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
}
testing.expect(t, found)
}
@(test)
contextual_inference_flows_through_numeric_arithmetic :: proc(t: ^testing.T) {
text := `take_u16 func(_ u16) void {}
take_f32 func(_ f32) void {}
runtime_seed f32 = 0
G :: 10
H u16 :: G + 2
GF :: 1.5
HF f32 :: GF + 2.5
CG :: 5
CFG :: 1.0
get func() f32 {
seed f32 :: 2.0
c :: seed + 3.0
d :: 4.0 + seed
return c + d + runtime_seed
}
main func() void {
a :: 10
b u16 :: a + 2
x :: 1.5
y f32 :: x + 2.5
z f32 :: 2.5 + x
call_i :: 7
call_f :: 1.25
take_u16(call_i + 3)
take_f32(call_f + 3.0)
take_u16(CG + 1)
take_f32(CFG + 1.0)
_ = b
_ = y
_ = z
_ = H
_ = HF
_ = get()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
global_ok := 0
for global in hir_module.globals {
name := symbol.resolve(&symbols, global.name)
switch name {
case "G", "H", "CG":
global_ok += 1 if types.equal(global.type, types.U16) else 0
case "GF", "HF", "CFG":
global_ok += 1 if types.equal(global.type, types.F32) else 0
}
}
testing.expect_value(t, global_ok, 6)
main_ok := 0
get_ok := false
for function in hir_module.functions {
name := symbol.resolve(&symbols, function.name)
if name == "get" {
get_ok = types.equal(function.result, types.F32)
for local in function.locals {
local_name := symbol.resolve(&symbols, local.name)
if local_name == "c" || local_name == "d" {
main_ok += 1 if types.equal(local.type, types.F32) else 0
}
}
} else if name == "main" {
for local in function.locals {
local_name := symbol.resolve(&symbols, local.name)
switch local_name {
case "a", "call_i":
main_ok += 1 if types.equal(local.type, types.U16) else 0
case "x", "call_f":
main_ok += 1 if types.equal(local.type, types.F32) else 0
}
}
}
}
testing.expect(t, get_ok)
testing.expect_value(t, main_ok, 6)
}
@(test)
contextual_inference_rejects_non_fitting_arithmetic_demand :: proc(t: ^testing.T) {
text := `BIG :: 100000
C u8 :: BIG + 1
main func() void {
_ = C
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
}
testing.expect(t, found)
}
@(test)
missing_qualified_signature_symbol_reports_one_root_error :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-root-diagnostic"
defer _ = os2.remove_all(directory)
_ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
token_text := `Token :: struct { start int }
`
lexer_text := `scan func(cursor usize) void ! missing.Error {
token Token = Token{start = cursor}
_ = token
}
`
main_text := `main func() void {
scan(1) catch |_| { return }
}
`
testing.expect(t, os.write_entire_file(
"/tmp/brolang-test-root-diagnostic/token.bro",
transmute([]byte)token_text,
))
testing.expect(t, os.write_entire_file(
"/tmp/brolang-test-root-diagnostic/lexer.bro",
transmute([]byte)lexer_text,
))
testing.expect(t, os.write_entire_file(
"/tmp/brolang-test-root-diagnostic/main.bro",
transmute([]byte)main_text,
))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load(directory, &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 1)
testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'")
formatted := source.format(&diagnostics, source.Diagnostic_Id(0))
defer delete(formatted)
testing.expect(t, strings.contains(formatted, "/lexer.bro:1:32"))
testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol"))
testing.expect(t, !strings.contains(formatted, "fallible expression"))
testing.expect(t, !strings.contains(formatted, "must be consumed"))
testing.expect(t, !strings.contains(formatted, "could not resolve the 'int' constraint"))
}
@(test)
poisoned_global_and_local_types_do_not_create_inference_fallbacks :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-poisoned-declarations"
defer _ = os2.remove_all(directory)
_ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
text := `bad missing.Global :: 1
main func() void {
value absent.Local = 1
_ = value
}
`
testing.expect(t, os.write_entire_file(
"/tmp/brolang-test-poisoned-declarations/main.bro",
transmute([]byte)text,
))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load(directory, &sources, &diagnostics, &symbols)
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 2)
unknown_missing := false
unknown_absent := false
for diagnostic in diagnostics.items {
unknown_missing = unknown_missing || diagnostic.message == "unknown symbol 'missing'"
unknown_absent = unknown_absent || diagnostic.message == "unknown symbol 'absent'"
testing.expect(t, !strings.contains(diagnostic.message, "could not resolve"))
testing.expect(t, !strings.contains(diagnostic.message, "could not infer"))
}
testing.expect(t, unknown_missing)
testing.expect(t, unknown_absent)
}
named_record_field_type :: proc(
module: ^hir.Module,
symbols: ^symbol.Table,
record_name, field_name: string,
) -> (types.Type, bool) {
record := types.find_named(&module.types, 0, u32(symbol.intern(symbols, record_name)))
if !types.is_record(record, &module.types) {
return types.INVALID, false
}
field_symbol := symbol.intern(symbols, field_name)
for field in types.fields_for(&module.types, record) {
if field.name == u32(field_symbol) {
return field.type, true
}
}
return types.INVALID, false
}
@(test)
native_record_constraint_fields_resolve_program_wide :: proc(t: ^testing.T) {
text := `Token :: struct { start int }
Backward :: struct { start int }
Wide :: struct { value int }
Literal :: struct { value int }
Measurement :: struct {
ratio float
span range
}
Payload :: union { count int }
take_usize func(value usize) usize { return value }
exercise func(cursor usize, narrow i8, wider i16, count i32) i32 {
token Token = Token{start = cursor}
wide Wide = Wide{value = narrow}
wide.value = wider
small Literal = Literal{value = 1}
large Literal = Literal{value = 1000}
backward Backward = Backward{start = 1}
measurement Measurement = Measurement{ratio = 1.5, span = 0..3}
payload Payload = Payload{count = count}
_ = token.start
_ = wide.value
_ = small.value
_ = large.value
_ = take_usize(backward.start)
_ = measurement.ratio
_ = measurement.span
_ = payload.count
return 0
}
main func() i32 { return exercise(7, 1, 1000, 3) }
`
source_file := source.Source{path="record_constraints.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
token_start, token_ok := named_record_field_type(&hir_module, &symbols, "Token", "start")
backward_start, backward_ok := named_record_field_type(&hir_module, &symbols, "Backward", "start")
wide_value, wide_ok := named_record_field_type(&hir_module, &symbols, "Wide", "value")
literal_value, literal_ok := named_record_field_type(&hir_module, &symbols, "Literal", "value")
ratio, ratio_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "ratio")
span, span_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "span")
count, count_ok := named_record_field_type(&hir_module, &symbols, "Payload", "count")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, token_ok && backward_ok && wide_ok && literal_ok && ratio_ok && span_ok && count_ok)
testing.expect_value(t, token_start, types.USIZE)
testing.expect_value(t, backward_start, types.USIZE)
testing.expect_value(t, wide_value, types.I16)
testing.expect_value(t, literal_value, types.I16)
testing.expect_value(t, ratio, types.F64)
testing.expect(t, types.is_range(span, &hir_module.types))
testing.expect_value(t, types.child_type(span, &hir_module.types), types.I8)
testing.expect_value(t, count, types.I32)
}
@(test)
native_record_int_field_compiles_and_runs_as_usize :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-record-field-usize"
main_path := "/tmp/brolang-test-record-field-usize/main.bro"
output := "/tmp/brolang-test-record-field-usize-output"
text := `Token :: struct { start int }
main func() i32 {
cursor usize = 7
token Token = Token{start = cursor}
if (token.start != cursor) return 1
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
native_record_constraint_fields_report_unresolved_and_conflicting_demands :: proc(t: ^testing.T) {
cases := [2]struct {
text: string,
needle: string,
}{
{
text = `Token :: struct { start int }
main func() void {}
`,
needle = "could not resolve the 'int' constraint for field 'Token.start'",
},
{
text = `Token :: struct { start int }
use func(signed i32, unsigned usize) void {
a Token = Token{start = signed}
b Token = Token{start = unsigned}
_ = a
_ = b
}
main func() void { use(1, 2) }
`,
needle = "conflicting types i32 and usize for field 'Token.start' declared as 'int'",
},
}
for test_case in cases {
source_file := source.Source{path="bad_record_constraint.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
ir_module := lower.lower(&hir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
matching := 0
for diagnostic in diagnostics.items {
matching += 1 if strings.contains(diagnostic.message, test_case.needle) else 0
}
testing.expect_value(t, matching, 1)
testing.expect(t, len(llvm_text) > 0)
delete(llvm_text)
ir.destroy_module(&ir_module)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
directory := "/tmp/brolang-test-record-field-conflict"
main_path := "/tmp/brolang-test-record-field-conflict/main.bro"
output := "/tmp/brolang-test-record-field-conflict-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)cases[1].text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 1)
testing.expect(t, !os.exists(output))
}
@(test)
constraint_fields_remain_rejected_outside_named_native_records :: proc(t: ^testing.T) {
text := `BadC :: c_struct { value int }
BadNested :: struct { values []int }
make_type func() type {
return struct { value int }
}
main func() void {
Generated :: @make_type()
_ = Generated
}
`
source_file := source.Source{path="excluded_record_constraints.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
record_errors := 0
anonymous_error := false
for diagnostic in diagnostics.items {
record_errors += 1 if strings.contains(diagnostic.message, "record fields must have runtime value types") else 0
anonymous_error = anonymous_error || strings.contains(diagnostic.message, "anonymous struct field 'value' requires a concrete runtime type")
}
c_field, c_ok := named_record_field_type(&hir_module, &symbols, "BadC", "value")
nested_field, nested_ok := named_record_field_type(&hir_module, &symbols, "BadNested", "values")
testing.expect_value(t, record_errors, 1)
testing.expect(t, anonymous_error)
testing.expect(t, c_ok && nested_ok)
testing.expect_value(t, c_field, types.INT)
testing.expect(t, types.is_slice(nested_field, &hir_module.types))
testing.expect_value(t, types.child_type(nested_field, &hir_module.types), types.INT)
}
@(test)
parser_records_native_tests_and_test_imports :: proc(t: ^testing.T) {
text := `test import "../math"
addition test {
return
}
`
source_file := source.Source{path="tests.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.imports), 1)
testing.expect(t, module.imports[0].test_only)
testing.expect_value(t, module.imports[0].alias, symbol.INVALID)
testing.expect_value(t, len(module.functions), 1)
testing.expect(t, module.functions[0].test)
testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "addition")
}
@(test)
native_test_framework_discovers_reports_and_preserves_main :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-native-framework"
root := "/tmp/brolang-test-native-framework/root"
dependency := "/tmp/brolang-test-native-framework/dependency"
root_path := "/tmp/brolang-test-native-framework/root/main.bro"
dependency_path := "/tmp/brolang-test-native-framework/dependency/math.bro"
test_output := "/tmp/brolang-test-native-framework-tests"
app_output := "/tmp/brolang-test-native-framework-app"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(test_output)
defer _ = os.remove(app_output)
testing.expect(t, os2.make_directory_all(root) == nil)
testing.expect(t, os2.make_directory_all(dependency) == nil)
root_text := `testing :: import "@std/testing"
test import "../dependency"
Token :: struct { start int }
OperationError :: enum { failed }
scan func() void {
cursor usize = 0
_ = Token{start = cursor}
}
operation func(fail bool) void ! OperationError {
if (fail) return .failed
}
main func() i32 {
scan()
return 77
}
root_passes test {
try operation(false)
try testing.expect(true)
}
root_fails test {
try testing.expect_equal(42, 41)
}
root_continues test {
try testing.expect(true)
}
root_errors test {
try operation(true)
}
`
dependency_text := `testing :: import "@std/testing"
dependency_passes test {
try testing.expect(true)
}
`
testing.expect(t, os.write_entire_file(root_path, transmute([]byte)root_text))
testing.expect(t, os.write_entire_file(dependency_path, transmute([]byte)dependency_text))
app_status := compiler_core.compile_package(
root, app_output, nil, target.DEFAULT, cimport.Options{}, ".",
)
testing.expect_value(t, app_status, 0)
app_state := run_executable(app_output)
testing.expect_value(t, app_state.exit_code, 77)
test_status := compiler_core.compile_package(
root, test_output, nil, target.DEFAULT, cimport.Options{}, ".", .Test,
)
testing.expect_value(t, test_status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{test_output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
output := string(stderr)
testing.expect_value(t, state.exit_code, 1)
testing.expect(t, strings.contains(output, "root.root_passes...[ok]"))
testing.expect(t, strings.contains(output, "root.root_fails...[failed]"))
testing.expect(t, strings.contains(output, "expected 42, found 41"))
testing.expect(t, strings.contains(output, "root.root_continues...[ok]"))
testing.expect(t, strings.contains(output, "root.root_errors...[failed]"))
testing.expect(t, strings.contains(output, "dependency.dependency_passes...[ok]"))
testing.expect(t, strings.contains(output, root_path))
testing.expect(t, strings.contains(output, "3 passed, 2 failed"))
}
@(test)
uint_constraint_parses_as_type_and_comptime_value :: proc(t: ^testing.T) {
text := `value uint :: 1
Constraint :: uint
`
source_file := source.Source{path="uint.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer 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.globals), 2)
testing.expect_value(t, module.globals[0].type, types.UINT)
testing.expect_value(t, module.exprs[module.globals[1].expr].kind, ast.Expr_Kind.Type)
testing.expect_value(t, module.exprs[module.globals[1].expr].type, types.UINT)
}
@(test)
uint_constraint_uses_smallest_unsigned_types_and_widens :: proc(t: ^testing.T) {
text := `Zero uint :: 0
Byte uint :: 255
Word uint :: 256
Dword uint :: 65536
Maximum uint :: 18446744073709551615
Counter uint = 1
widen func() uint {
value uint = 1
value = 1000
return value
}
widen_global func() void { Counter = 1000 }
main func() void {
_ = Zero
_ = Byte
_ = Word
_ = Dword
_ = Maximum
_ = widen()
widen_global()
}
`
source_file := source.Source{path="uint_types.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
expected := []types.Type{types.U8, types.U8, types.U16, types.U32, types.U64, types.U16}
for value_type, index in expected {
testing.expect_value(t, hir_module.globals[index].type, value_type)
}
widen_symbol := symbol.intern(&symbols, "widen")
found_widen := false
for function in hir_module.functions {
if function.name != widen_symbol {
continue
}
found_widen = true
testing.expect_value(t, function.result, types.U16)
testing.expect_value(t, function.locals[0].type, types.U16)
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_widen)
}
@(test)
uint_constraint_infers_boundaries_calls_and_records :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-uint-constraint"
main_path := "/tmp/brolang-test-uint-constraint/main.bro"
output := "/tmp/brolang-test-uint-constraint-output"
text := `Box :: struct { value uint }
identity func(value uint) uint { return value }
whole func(value int) int { return value }
max_value func() uint { return 18446744073709551615 }
make_box func(value usize) Box { return Box{value = value} }
main func() i32 {
zero uint :: 0
byte uint :: 255
word uint :: 256
word_max uint :: 65535
dword uint :: 65536
maximum uint :: 18446744073709551615
platform usize = 7
c_value c_uint = c_uint(9)
box Box = make_box(7)
signed isize = -1
if (identity(zero) != 0) return 1
if (identity(byte) != 255) return 2
if (identity(word) != 256) return 3
if (identity(word_max) != 65535) return 4
if (identity(dword) != 65536) return 5
if (identity(maximum) != 18446744073709551615) return 6
if (box.value != 7) return 7
if (signed != -1) return 8
if (identity(platform) != 7) return 9
if (identity(c_value) != c_value) return 10
if (whole(word) != 256) return 11
if (max_value() != 18446744073709551615) return 12
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
uint_constraint_rejects_other_families_and_recovers_records_as_u64 :: proc(t: ^testing.T) {
text := `Unresolved :: struct { value uint }
Conflict :: struct { value uint }
take func(value uint) uint { return value }
bad_result func() uint { return -1 }
bad func(signed i32, decimal f64) void {
_ = take(signed)
_ = take(decimal)
negative uint :: -1
a Conflict = Conflict{value = signed}
_ = negative
_ = a
}
Overflow uint :: 18446744073709551616
NegativeGlobal uint :: -1
main func() void {
bad(1, 1.0)
_ = bad_result()
}
`
source_file := source.Source{path="bad_uint.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
signed_error := false
float_error := false
negative_error := false
field_error := false
overflow_error := false
global_error := false
result_error := false
for diagnostic in diagnostics.items {
signed_error = signed_error || strings.contains(diagnostic.message, "cannot pass i32 to 'uint' parameter 'value'")
float_error = float_error || strings.contains(diagnostic.message, "cannot pass f64 to 'uint' parameter 'value'")
negative_error = negative_error || strings.contains(diagnostic.message, "could not resolve the 'uint' constraint for local 'negative'")
field_error = field_error || strings.contains(diagnostic.message, "type i32 does not satisfy the 'uint' constraint for field 'Conflict.value'")
overflow_error = overflow_error || strings.contains(diagnostic.message, "magnitude does not fit in u64")
global_error = global_error || strings.contains(diagnostic.message, "could not resolve the 'uint' constraint for global 'NegativeGlobal'")
result_error = result_error || strings.contains(diagnostic.message, "could not resolve a concrete result type for 'bad_result'")
}
unresolved, unresolved_ok := named_record_field_type(&hir_module, &symbols, "Unresolved", "value")
testing.expect(t, signed_error && float_error && negative_error && field_error && overflow_error && global_error && result_error)
testing.expect(t, unresolved_ok)
testing.expect_value(t, unresolved, types.U64)
}
@(test)
milestone_44_struct_field_defaults_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-struct-field-defaults"
main_path := "/tmp/brolang-test-struct-field-defaults/main.bro"
output := "/tmp/brolang-test-struct-field-defaults-output"
text := `Config :: struct {
count int = 7
enabled bool = true
name []u8 = "bro"
required i32
}
Lists :: struct { items [][]u8 = &[] }
Generated func($T type, $initial i32) type {
return struct { value T = initial }
}
read func(value Config) i32 {
return value.required + value.count
}
ANSWER :: $read(Config{required = 35})
main func() i32 {
defaults Config = Config{required = 35}
overridden Config = Config{count = 1, enabled = false, name = "x", required = 40}
lists Lists = Lists{}
generated Generated(i32, 9) = Generated(i32, 9){}
if (ANSWER != 42) return 1
if (defaults.count != 7 or defaults.enabled == false) return 2
if (defaults.name.len != 3 or lists.items.len != 0) return 3
if (overridden.count != 1 or overridden.enabled or overridden.name.len != 1) return 4
if (generated.value != 9) return 5
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
dependent_comptime_callbacks_and_imported_grouped_sums_compile :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-dependent-callbacks"
app := "/tmp/brolang-test-dependent-callbacks/app"
dependency := "/tmp/brolang-test-dependent-callbacks/dependency"
app_path := "/tmp/brolang-test-dependent-callbacks/app/main.bro"
dependency_path := "/tmp/brolang-test-dependent-callbacks/dependency/errors.bro"
output := "/tmp/brolang-test-dependent-callbacks-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os2.make_directory_all(app) == nil)
testing.expect(t, os2.make_directory_all(dependency) == nil)
dependency_text := `AllocError :: enum { out_of_memory }
`
app_text := `dependency :: import "../dependency"
PutError :: enum { key_exists }
Entry func($K, $V type) type {
return struct { key K, value V }
}
Map func(
$K, $V type,
$hash_key func(key K) usize,
$keys_eql func(a, b K) bool,
) type {
return struct { entry Entry(K, V) }
}
hash func(key []u8) usize { return key.len }
eql func(a, b []u8) bool { return a.len == b.len }
StringMap func($V type) type {
return Map([]u8, V, hash, eql)
}
make func(
$K, $V type,
$hash_key func(key K) usize,
$keys_eql func(a, b K) bool,
) Map(K, V, hash_key, keys_eql) {
return Map(K, V, hash_key, keys_eql) {
entry = Entry(K, V) {key = "", value = 0},
}
}
put func(
$K, $V type,
$hash_key func(key K) usize,
$keys_eql func(a, b K) bool,
map @Map(K, V, hash_key, keys_eql),
key K,
) void {
_ = map
_ = key
}
fallible func() void ! (PutError | dependency.AllocError) { return .key_exists }
main func() void {
map StringMap(u32) = make()
put(&map, "key")
_ = map
fallible() catch |_| {}
}
`
testing.expect(t, os.write_entire_file(dependency_path, transmute([]byte)dependency_text))
testing.expect(t, os.write_entire_file(app_path, transmute([]byte)app_text))
testing.expect_value(t, compiler_core.compile_package(app, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
anonymous_keyed_records_infer_structural_types_and_preserve_tuples :: proc(t: ^testing.T) {
text := `first :: {x = 1, name = "bro"}
second :: {x = 2, name = "hey"}
empty :: {}
pair :: {1, 2}
main func() i32 {
if first.x + second.x != 3 { return 1 }
if first.name.len != 3 or second.name.len != 3 { return 2 }
return pair.0 - 1
}
`
source_file := source.Source{path="anonymous_records.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
first_expr := ast_module.exprs[ast_module.globals[0].expr]
empty_expr := ast_module.exprs[ast_module.globals[2].expr]
pair_expr := ast_module.exprs[ast_module.globals[3].expr]
testing.expect_value(t, first_expr.kind, ast.Expr_Kind.Struct_Literal)
testing.expect(t, !first_expr.tuple)
testing.expect(t, empty_expr.tuple)
testing.expect(t, pair_expr.tuple)
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, hir_module.globals[1].type))
testing.expect(t, types.is_record(hir_module.globals[0].type, &hir_module.types))
empty_type, empty_ok := types.node(&hir_module.types, hir_module.globals[2].type)
pair_type, pair_ok := types.node(&hir_module.types, hir_module.globals[3].type)
testing.expect(t, empty_ok && empty_type.tuple)
testing.expect(t, pair_ok && pair_type.tuple)
bare_source := source.Source{path="bare_anonymous_record.bro", text="bad :: {value = 1, missing}\n"}
bare_diagnostics := source.init_diagnostics(&bare_source)
defer source.destroy_diagnostics(&bare_diagnostics)
bare_symbols := symbol.init_table()
defer symbol.destroy_table(&bare_symbols)
bare_stream := lexer.lex(&bare_source, &bare_diagnostics, &bare_symbols)
defer delete(bare_stream.items)
bare_module := parser.parse(&bare_stream, &bare_source, &bare_diagnostics)
defer ast.destroy_module(&bare_module)
found_bare := false
for diagnostic in bare_diagnostics.items {
found_bare = found_bare || strings.contains(diagnostic.message, "anonymous record fields require '= value'")
}
testing.expect(t, found_bare)
}
@(test)
enum_field_struct_and_contextual_anonymous_records_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-enum-field-struct"
main_path := "/tmp/brolang-test-enum-field-struct/main.bro"
output := "/tmp/brolang-test-enum-field-struct-output"
text := `meta :: import "@std/meta"
testing :: import "@std/testing"
OtherTokenKind :: enum { alpha, beta, end }
TokenKind :: enum(u8) {
ident = 3
int = 8
eof = 21
}
Simple :: enum { first, second }
Names :: alias meta.EnumFieldStruct(TokenKind, ?[]u8, some!(null))
Flags :: alias meta.EnumFieldStruct(Simple, bool, false)
Direct :: alias struct_type!(.auto, {"x", "name"}, {i32, []u8}, {null, "bro"})
CPoint :: alias struct_type!(.c, {"x"}, {c_int}, {null})
ArrayInput :: alias struct_type!(.auto, ["value"], [i32], [7])
Empty :: alias struct_type!(.auto, {}, {}, {})
direct_position struct_type!(.auto, {"value"}, {i32}, {null}) = {value = 5}
make_direct func() struct_type!(.auto, {"value"}, {i32}, {null}) {
return {value = 6}
}
Generated func($T type) type {
names [1]mut []u8 = undefined
field_types [1]mut type = undefined
defaults [1]mut ?T = undefined
names[0] = "value"
field_types[0] = T
defaults[0] = 42
return struct_type!(.auto, names, field_types, defaults)
}
GeneratedInt :: alias Generated(i32)
ordered Names = {}
static_none ?i32 :: null
static_some ?i32 :: 1
none :: 41
Map func($E, $V type) type {
match typeinfo!(E) {
.enum |info|: return struct {
present [info.fields.len]mut bool
values [info.fields.len]mut V
}
else: compile_error!("EnumMap key must be an enum")
}
}
init func($E, $V type, values meta.EnumFieldStruct(E, ?V, some!(null))) Map(E, V) {
map Map(E, V) = undefined
match typeinfo!(E) {
.enum |info|: inline for info.fields |field, index| {
map.present[index] = false
if field!(values, field.name) |value| {
map.present[index] = true
map.values[index] = value
}
}
else: compile_error!("EnumMap key must be an enum")
}
return map
}
get func($E, $V type, map @Map(E, V), key E) ?V {
match typeinfo!(E) {
.enum |info|: inline for info.fields |field, index| {
if key == field!(E, field.name) {
if map.present[index] { return map.values[index] }
return null
}
}
else: compile_error!("EnumMap key must be an enum")
}
}
main func() i32 {
other Map(OtherTokenKind, []u8) = init(OtherTokenKind, []u8, {alpha = "other"})
if !other.present[0] { return 26 }
_ = ordered
if none != 41 { return 24 }
if !$(static_none == null) or !$(null == static_none) or
$(static_some == null) or $(null == static_some) { return 23 }
inferred :: {x = 40, name = "bro"}
if inferred.x != 40 or inferred.name.len != 3 { return 1 }
direct Direct = {x = 7}
if direct.x != 7 or direct.name.len != 3 { return 10 }
generated GeneratedInt = {}
if generated.value != 42 { return 11 }
generated_again Generated(i32) = generated
if generated_again.value != 42 { return 15 }
array_input ArrayInput = {}
if array_input.value != 7 { return 16 }
empty_record Empty = {}
_ = empty_record
if direct_position.value != 5 or make_direct().value != 6 { return 17 }
c_point CPoint = {x = 9}
if c_point.x != 9 { return 12 }
nested ??i32 = some!(null)
if nested |inner| {
if inner |_| { return 13 }
} else { return 14 }
names Names = {
ident = "identifier",
int = "integer",
}
if field!(names, "ident") |value| {
if value.len != 10 { return 2 }
} else { return 3 }
if field!(names, "int") |value| {
if value.len != 7 { return 4 }
} else { return 5 }
if field!(names, "eof") |_| { return 6 }
empty Names = {}
if field!(empty, "ident") |_| { return 7 }
flags Flags = {second = true}
if flags.first or !flags.second { return 8 }
map Map(TokenKind, []u8) = init({
ident = "identifier",
int = "integer",
})
if !map.present[0] or !map.present[1] or map.present[2] { return 9 }
if map.values[0].len != 10 or map.values[1].len != 7 { return 18 }
ident :: get(&map, TokenKind.ident)
if ident == null or null == ident { return 19 }
if ident |value| {
if value.len != 10 { return 19 }
} else { return 20 }
eof :: get(&map, TokenKind.eof)
if eof != null or null != eof { return 21 }
location testing.SourceLocation = {file = "test.bro", line = 1, column = 1}
testing.expect_equal(null, eof, location) catch |_| { return 24 }
testing.expect_equal(ident, ident, location) catch |_| { return 25 }
if eof |_| { return 22 }
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load(
directory, &sources, &diagnostics, &symbols, project_root_path=".",
)
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
names_type := types.INVALID
ordered_symbol := symbol.intern(&symbols, "ordered")
for global in hir_module.globals {
if global.name == ordered_symbol {
names_type = types.resolve_alias(global.type, &hir_module.types)
break
}
}
names_fields := types.fields_for(&hir_module.types, names_type)
testing.expect_value(t, len(names_fields), 3)
if len(names_fields) == 3 {
testing.expect_value(t, names_fields[0].name, u32(symbol.intern(&symbols, "ident")))
testing.expect_value(t, names_fields[1].name, u32(symbol.intern(&symbols, "int")))
testing.expect_value(t, names_fields[2].name, u32(symbol.intern(&symbols, "eof")))
}
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".",
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
std_meta_tests_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-std-meta"
main_path := "/tmp/brolang-test-std-meta/main.bro"
output := "/tmp/brolang-test-std-meta-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
text := `test import "@std/meta"
`
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".", .Test,
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
array_reflection_reports_child_and_logical_length :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-array-reflection"
main_path := "/tmp/brolang-test-array-reflection/main.bro"
output := "/tmp/brolang-test-array-reflection-output"
text := `meta :: import "@std/meta"
Alias :: alias [3]u16
matches func($Array, $Child type, $len usize) bool {
match typeinfo!(Array) {
.array |info|: return info.child == Child and info.len == len
else: return false
}
}
main func() i32 {
if !$(matches([4]i32, i32, 4)) { return 1 }
if !$(matches([0]bool, bool, 0)) { return 2 }
if !$(matches(Alias, u16, 3)) { return 3 }
if !$(matches([2]mut i64, i64, 2)) { return 4 }
if !$(matches([2;0]u8, u8, 2)) { return 5 }
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".",
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
distinct_reflection_reports_immediate_backing :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-distinct-reflection"
main_path := "/tmp/brolang-test-distinct-reflection/main.bro"
output := "/tmp/brolang-test-distinct-reflection-output"
text := `meta :: import "@std/meta"
Inner :: distinct u16
Outer :: distinct Inner
OuterAlias :: alias Outer
matches func($Distinct, $Backing type) bool {
match typeinfo!(Distinct) {
.distinct |backing|: return backing == Backing
else: return false
}
}
main func() i32 {
if !$(matches(Inner, u16)) { return 1 }
if !$(matches(Outer, Inner)) { return 2 }
if !$(matches(OuterAlias, Inner)) { return 3 }
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".",
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
static_string_map_infers_array_size_and_preserves_promoted_backing :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-static-string-map"
main_path := "/tmp/brolang-test-static-string-map/main.bro"
output := "/tmp/brolang-test-static-string-map-output"
text := `std :: import "@std"
static_string_map :: import "@std/static_string_map"
TokenKind :: enum {
keyword_if
keyword_else
keyword_for
keyword_return
keyword_while
}
keywords std.StaticStringMap(TokenKind) :: static_string_map.init([
{"if", .keyword_if},
{"else", .keyword_else},
{"for", .keyword_for},
{"return", .keyword_return},
])
fallback :: static_string_map.init(TokenKind, [
{"while", .keyword_while},
])
empty std.StaticStringMap(TokenKind) :: static_string_map.init([])
numbers []i32 = ${
values [3]mut i32 = [7, 8, 9]
yield values[..]
}
main func() i32 {
if keywords.keys.len != 4 or keywords.values.len != 4 or keywords.len_indexes.len != 7 { return 1 }
if keywords.min_len != 2 or keywords.max_len != 6 { return 17 }
if empty.keys.len != 0 or empty.values.len != 0 or empty.len_indexes.len != 0 { return 18 }
if numbers.len != 3 or numbers[0] != 7 or numbers[2] != 9 { return 19 }
if static_string_map.get(&keywords, "if") |value| {
if value != TokenKind.keyword_if { return 2 }
} else { return 3 }
if static_string_map.get(&keywords, "else") |value| {
if value != TokenKind.keyword_else { return 4 }
} else { return 5 }
if static_string_map.get(&keywords, "for") |value| {
if value != TokenKind.keyword_for { return 6 }
} else { return 7 }
if static_string_map.get(&keywords, "return") |value| {
if value != TokenKind.keyword_return { return 8 }
} else { return 9 }
if static_string_map.get(&keywords, "no") |_| { return 10 }
if static_string_map.get(&keywords, "four") |_| { return 11 }
if static_string_map.get(&keywords, "x") |_| { return 12 }
if static_string_map.get(&keywords, "longer-than-any-key") |_| { return 13 }
if static_string_map.get(&empty, "if") |_| { return 14 }
if static_string_map.get(&fallback, "while") |value| {
if value != TokenKind.keyword_while { return 15 }
} else { return 16 }
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".",
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
static_string_map_reports_duplicate_and_malformed_entries :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-static-string-map-errors"
main_path := "/tmp/brolang-test-static-string-map-errors/main.bro"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
duplicate_text := `std :: import "@std"
static_string_map :: import "@std/static_string_map"
TokenKind :: enum { keyword_if, keyword_else }
bad std.StaticStringMap(TokenKind) = static_string_map.init([
{"if", .keyword_if},
{"if", .keyword_else},
])
main func() void {}
`
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)duplicate_text))
{
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".")
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 in diagnostics.items {
found = found || strings.contains(diagnostic.message, "duplicate static string map key")
}
testing.expect(t, found)
}
malformed_text := `std :: import "@std"
static_string_map :: import "@std/static_string_map"
TokenKind :: enum { keyword_if }
bad std.StaticStringMap(TokenKind) = static_string_map.init([
{123, .keyword_if},
])
bad_value std.StaticStringMap(TokenKind) = static_string_map.init([
{"if", "bad"},
])
main func() void {}
`
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)malformed_text))
{
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".")
defer ast.destroy_module(&module)
hir_module := checker.check(&module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
found_key := false
found_value := false
for diagnostic in diagnostics.items {
found_key = found_key || strings.contains(diagnostic.message, "cannot implicitly convert i8 to []u8 at comptime")
found_value = found_value ||
strings.contains(diagnostic.message, "cannot implicitly convert") &&
strings.contains(diagnostic.message, "to TokenKind at comptime")
}
testing.expect(t, found_key)
testing.expect(t, found_value)
}
}
@(test)
noreturn_functions_function_pointers_and_peer_types_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-noreturn"
main_path := "/tmp/brolang-test-noreturn/main.bro"
output := "/tmp/brolang-test-noreturn-output"
text := `meta :: import "@std/meta"
die func() noreturn {
unreachable
}
forward func() noreturn {
die()
}
choose func(flag bool) i32 {
return if flag { yield 42 } else { yield die() }
}
reflects_bottom func($T type) bool {
match typeinfo!(T) {
.noreturn: return true
else: return false
}
}
main func() i32 {
callback @func() noreturn = forward
_ = callback
if choose(true) != 42 { return 1 }
if !reflects_bottom(noreturn) { return 2 }
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".",
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
unreachable_traps_at_runtime_and_fails_at_comptime :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-unreachable"
main_path := "/tmp/brolang-test-unreachable/main.bro"
output := "/tmp/brolang-test-unreachable-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
runtime_text := `main func() void {
unreachable
}
`
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)runtime_text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state, stdout, stderr, err := os2.process_exec(
os2.Process_Desc{command=[]string{output}}, context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect(t, err == nil)
testing.expect(t, !state.success)
testing.expect(t, strings.contains(string(stderr), "runtime trap: reached unreachable code"))
comptime_text := `value :: $unreachable
main func() void {}
`
source_file := source.Source{path="unreachable_comptime.bro", text=comptime_text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&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, "reached unreachable code during comptime evaluation")
}
testing.expect(t, found)
}
@(test)
noreturn_storage_c_abi_and_fallthrough_are_rejected :: proc(t: ^testing.T) {
text := `Bad :: struct { value noreturn }
foreign c_func() noreturn
fallthrough func() noreturn {}
parameter func(value noreturn) void { _ = value }
main func() void {
if false { fallthrough() }
stored noreturn = unreachable
}
`
source_file := source.Source{path="invalid_noreturn.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
field, c_abi, missing_return, parameter, storage := false, false, false, false, false
for diagnostic in diagnostics.items {
field = field || strings.contains(diagnostic.message, "record fields must have runtime value types")
c_abi = c_abi || strings.contains(diagnostic.message, "noreturn is not supported across the C ABI")
missing_return = missing_return || strings.contains(diagnostic.message, "does not return a value")
parameter = parameter || strings.contains(diagnostic.message, "only valid as a native function result type")
storage = storage || strings.contains(diagnostic.message, "cannot store a noreturn value")
}
testing.expect(t, field && c_abi && missing_return && parameter && storage)
}
@(test)
anonymous_records_and_struct_type_report_targeted_errors :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-bad-struct-type"
main_path := "/tmp/brolang-test-bad-struct-type/main.bro"
text := `meta :: import "@std/meta"
E :: enum { value }
Fields :: alias struct_type!(.auto, {"value"}, {i32}, {null})
RequiredEnum :: alias meta.EnumFieldStruct(E, i32, null)
NotEnum :: alias meta.EnumFieldStruct(i32, i32, null)
BadField :: alias struct_type!(.auto, {"value"}, {void}, {null})
BadComptimeField :: alias struct_type!(.auto, {"T"}, {type}, {null})
BadDefault :: alias struct_type!(.auto, {"value"}, {i32}, {"no"})
UnstableDefault :: alias struct_type!(.auto, {"value"}, {i32}, {undefined})
BadLengths :: alias struct_type!(.auto, {"one", "two"}, {i32}, {null})
BadNames :: alias struct_type!(.auto, {"not-valid"}, {i32}, {null})
DuplicateNames :: alias struct_type!(.auto, {"same", "same"}, {i32, i32}, {null, null})
BadLayout :: alias struct_type!(.packed, {"value"}, {i32}, {null})
BadC :: alias struct_type!(.c, {"value"}, {[]u8}, {null})
EmptyC :: alias struct_type!(.c, {}, {}, {})
unknown Fields = {missing = 1}
duplicate Fields = {value = 1, value = 2}
missing Fields = {}
missing_enum RequiredEnum = {}
not_enum NotEnum = {}
bad_field BadField = {}
bad_comptime_field BadComptimeField = {}
bad_default BadDefault = {}
unstable_default UnstableDefault = {}
bad_lengths BadLengths = {}
bad_names BadNames = {}
duplicate_names DuplicateNames = {}
bad_layout BadLayout = {}
bad_c BadC = {}
empty_c EmptyC = {}
main func() void {
_ = some!(1)
_ = unknown
_ = duplicate
_ = missing
_ = missing_enum
_ = not_enum
_ = bad_field
_ = bad_comptime_field
_ = bad_default
_ = unstable_default
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".")
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect(t, loaded)
found_non_enum := false
found_bad_field := false
found_bad_comptime_field := false
found_bad_default := false
found_unstable := false
found_unknown := false
found_duplicate := false
found_lengths := false
found_names := false
found_layout := false
found_c := false
found_some_context := false
found_missing := false
found_empty_c := false
for diagnostic in diagnostics.items {
found_non_enum = found_non_enum || strings.contains(diagnostic.message, "EnumFieldStruct key must be an enum")
found_bad_field = found_bad_field || strings.contains(diagnostic.message, "field 'value' has invalid value type")
found_bad_comptime_field = found_bad_comptime_field || strings.contains(diagnostic.message, "field 'T' has invalid value type")
found_bad_default = found_bad_default || strings.contains(diagnostic.message, "cannot implicitly convert")
found_unstable = found_unstable || strings.contains(diagnostic.message, "must have a stable comptime identity")
found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown struct field 'missing'")
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate initializer for struct field 'value'")
found_lengths = found_lengths || strings.contains(diagnostic.message, "collection lengths differ")
found_names = found_names || strings.contains(diagnostic.message, "field names must be valid") || strings.contains(diagnostic.message, "duplicate struct_type! field name")
found_layout = found_layout || strings.contains(diagnostic.message, "layout must be .auto or .c")
found_c = found_c || strings.contains(diagnostic.message, "invalid C-layout type")
found_some_context = found_some_context || strings.contains(diagnostic.message, "some! requires an optional context")
found_missing = found_missing || strings.contains(diagnostic.message, "missing initializer for struct field 'value'")
found_empty_c = found_empty_c || strings.contains(diagnostic.message, "c struct types require at least one field")
}
testing.expect(t, found_non_enum)
testing.expect(t, found_bad_field)
testing.expect(t, found_bad_comptime_field)
testing.expect(t, found_bad_default)
testing.expect(t, found_unstable)
testing.expect(t, found_unknown)
testing.expect(t, found_duplicate)
testing.expect(t, found_lengths)
testing.expect(t, found_names)
testing.expect(t, found_layout)
testing.expect(t, found_c)
testing.expect(t, found_some_context)
testing.expect(t, found_missing)
testing.expect(t, found_empty_c)
}
@(test)
type_factory_inference_prefers_unique_provenance_before_convertible_direct_evidence :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-factory-provenance-conversion"
main_path := "/tmp/brolang-test-factory-provenance-conversion/main.bro"
output := "/tmp/brolang-test-factory-provenance-conversion-output"
text := `HashBox func($K, $V type, $hash func(value K) usize) type {
return struct { value V }
}
string_hash func(value []u8) usize { return value.len }
StringBox func($V type) type {
return HashBox([]u8, V, string_hash)
}
put func(
$K, $V type,
$hash func(value K) usize,
box @mut HashBox(K, V, hash),
key K,
value V,
) void {
_ = box
_ = key
_ = value
}
main func() i32 {
box StringBox(u32) = {value = 0}
key []mut u8 = undefined
put(&box, key, 42)
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".",
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
assertion_location_injection_survives_expression_store_growth :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-assertion-location-growth"
main_path := "/tmp/brolang-test-assertion-location-growth/main.bro"
output := "/tmp/brolang-test-assertion-location-growth-output"
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
strings.write_string(&builder, "testing :: import \"@std/testing\"\n\nmany test {\n")
for index in 0..<128 {
fmt.sbprintf(&builder, "\ttry testing.expect_equal(%d, %d)\n", index, index)
}
strings.write_string(&builder, "}\n")
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)strings.to_string(builder)))
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".", .Test,
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}