13066 lines
443 KiB
Odin
13066 lines
443 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)
|
|
parser_accepts_grouped_params_and_multiline_statements :: proc(t: ^testing.T) {
|
|
text := `sum func(a,
|
|
b int) int {
|
|
return (a
|
|
+ b)
|
|
}
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(module.functions), 2)
|
|
testing.expect_value(t, len(module.functions[0].params), 2)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_comptime_value_params :: proc(t: ^testing.T) {
|
|
text := `make func($N usize, value i32) i32 {
|
|
return value
|
|
}
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found_dollar := false
|
|
for tok in stream.items {
|
|
found_dollar = found_dollar || tok.kind == token.Kind.Dollar
|
|
}
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found_dollar)
|
|
testing.expect_value(t, len(module.functions[0].params), 2)
|
|
testing.expect(t, module.functions[0].params[0].comptime_value)
|
|
testing.expect(t, !module.functions[0].params[1].comptime_value)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_comptime_type_params_and_builtin_type_args :: proc(t: ^testing.T) {
|
|
text := `id func($T type, value T) T {
|
|
return value
|
|
}
|
|
main func() void {
|
|
_ = id(i32, 42)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
call := module.exprs[module.statements[module.functions[1].body[0]].expr]
|
|
type_item, type_ok := types.node(&module.type_store, module.functions[0].params[0].type)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, module.functions[0].params[0].comptime_value)
|
|
testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].params[0].name), "T")
|
|
testing.expect(t, type_ok)
|
|
testing.expect_value(t, symbol.resolve(&symbols, symbol.Id(type_item.name)), "type")
|
|
testing.expect_value(t, call.kind, ast.Expr_Kind.Call)
|
|
testing.expect_value(t, module.exprs[call.args[0]].kind, ast.Expr_Kind.Type)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_sentinel_many_item_pointer_types :: proc(t: ^testing.T) {
|
|
text := `zero func(value [*;0]u8) void {}
|
|
newline func(value [*;'\n']mut u8) void {}
|
|
nullable func(value ?[*;0]u8) void {}
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
zero, zero_ok := types.node(&module.type_store, module.functions[0].params[0].type)
|
|
newline, newline_ok := types.node(&module.type_store, module.functions[1].params[0].type)
|
|
nullable, nullable_ok := types.node(&module.type_store, module.functions[2].params[0].type)
|
|
nullable_child, nullable_child_ok := types.node(&module.type_store, nullable.child)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, zero_ok && zero.kind == .Pointer && zero.many && zero.has_sentinel && zero.sentinel == 0)
|
|
testing.expect(t, newline_ok && newline.kind == .Pointer && newline.many && newline.mutable &&
|
|
newline.has_sentinel && newline.sentinel == '\n')
|
|
testing.expect(t, nullable_ok && nullable.kind == .Optional)
|
|
testing.expect(t, nullable_child_ok && nullable_child.kind == .Pointer &&
|
|
nullable_child.many && nullable_child.has_sentinel)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_c_function_pointer_types :: proc(t: ^testing.T) {
|
|
text := `take c_func(callback ?*c_func(value c_int) c_int) void
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
optional, optional_ok := types.node(&module.type_store, module.functions[0].params[0].type)
|
|
pointer, pointer_ok := types.node(&module.type_store, optional.child)
|
|
function, function_ok := types.node(&module.type_store, pointer.child)
|
|
params := types.params_for(&module.type_store, pointer.child)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, optional_ok && optional.kind == .Optional)
|
|
testing.expect(t, pointer_ok && pointer.kind == .Pointer && pointer.many && !pointer.mutable)
|
|
testing.expect(t, function_ok && function.kind == .Function && function.c_abi && !function.variadic)
|
|
testing.expect(t, function.child == types.C_INT)
|
|
testing.expect_value(t, len(params), 1)
|
|
testing.expect(t, params[0].type == types.C_INT)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_native_function_pointer_types :: proc(t: ^testing.T) {
|
|
text := `Error :: enum {
|
|
bad
|
|
}
|
|
take func(callback ?@func(value i32) i32, fallible @func() i32 ! Error) void
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
optional, optional_ok := types.node(&module.type_store, module.functions[0].params[0].type)
|
|
pointer, pointer_ok := types.node(&module.type_store, optional.child)
|
|
function, function_ok := types.node(&module.type_store, pointer.child)
|
|
params := types.params_for(&module.type_store, pointer.child)
|
|
fallible_pointer, fallible_pointer_ok := types.node(&module.type_store, module.functions[0].params[1].type)
|
|
fallible_function, fallible_function_ok := types.node(&module.type_store, fallible_pointer.child)
|
|
fallible, fallible_ok := types.node(&module.type_store, fallible_function.child)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, optional_ok && optional.kind == .Optional)
|
|
testing.expect(t, pointer_ok && pointer.kind == .Pointer && !pointer.many && !pointer.mutable)
|
|
testing.expect(t, function_ok && function.kind == .Function && !function.c_abi && !function.variadic)
|
|
testing.expect(t, function.child == types.I32)
|
|
testing.expect_value(t, len(params), 1)
|
|
testing.expect(t, params[0].type == types.I32)
|
|
testing.expect(t, fallible_pointer_ok && fallible_pointer.kind == .Pointer && !fallible_pointer.many)
|
|
testing.expect(t, fallible_function_ok && fallible_function.kind == .Function && !fallible_function.c_abi)
|
|
testing.expect(t, fallible_ok && fallible.kind == .Fallible && fallible.child == types.I32)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_c_function_pointer_alias_types :: proc(t: ^testing.T) {
|
|
text := `callback_alias :: alias ?*c_func(value i32) i32
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
name := symbol.intern(&symbols, "callback_alias")
|
|
alias := types.find_named(&module.type_store, 0, u32(name))
|
|
alias_node, alias_ok := types.node(&module.type_store, alias)
|
|
optional, optional_ok := types.node(&module.type_store, alias_node.child)
|
|
pointer, pointer_ok := types.node(&module.type_store, optional.child)
|
|
function, function_ok := types.node(&module.type_store, pointer.child)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, alias_ok && alias_node.kind == .Alias)
|
|
testing.expect(t, optional_ok && optional.kind == .Optional)
|
|
testing.expect(t, pointer_ok && pointer.kind == .Pointer)
|
|
testing.expect(t, function_ok && function.kind == .Function && function.c_abi)
|
|
}
|
|
|
|
@(test)
|
|
parser_rejects_old_function_declaration_binding_syntax :: proc(t: ^testing.T) {
|
|
text := `main :: func() void {}
|
|
foreign :: c_func() i32
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found := 0
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "function declarations do not use '::'") {
|
|
found += 1
|
|
}
|
|
}
|
|
testing.expect_value(t, found, 2)
|
|
testing.expect_value(t, len(module.functions), 2)
|
|
}
|
|
|
|
@(test)
|
|
parser_diagnoses_malformed_sentinel_pointer_types :: proc(t: ^testing.T) {
|
|
text := `bad func(value [*0]u8) void {}
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "expected ';' after '*' in sentinel pointer type")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_single_statement_one_line_functions :: proc(t: ^testing.T) {
|
|
text := `give func() i8 { return 7 }
|
|
main func() void { _ = give() }
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(module.functions), 2)
|
|
testing.expect_value(t, len(module.functions[0].body), 1)
|
|
testing.expect_value(t, len(module.functions[1].body), 1)
|
|
testing.expect_value(t, module.statements[module.functions[0].body[0]].kind, ast.Stmt_Kind.Return)
|
|
testing.expect_value(t, module.statements[module.functions[1].body[0]].kind, ast.Stmt_Kind.Assignment)
|
|
}
|
|
|
|
@(test)
|
|
parser_distinguishes_bodyless_declarations_and_definitions :: proc(t: ^testing.T) {
|
|
text := `foreign c_func(value i32) i32
|
|
defined c_func(value i32) i32
|
|
{
|
|
return value
|
|
}
|
|
native func() i32
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(module.functions), 4)
|
|
testing.expect(t, !module.functions[0].has_body)
|
|
testing.expect(t, module.functions[0].c_abi)
|
|
testing.expect(t, module.functions[1].has_body)
|
|
testing.expect(t, module.functions[1].c_abi)
|
|
testing.expect_value(t, len(module.functions[1].body), 1)
|
|
testing.expect(t, !module.functions[2].has_body)
|
|
testing.expect(t, !module.functions[2].c_abi)
|
|
testing.expect(t, module.functions[3].has_body)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_terminal_c_variadic_markers_and_recovers_nonterminal_markers :: proc(t: ^testing.T) {
|
|
text := `fixed c_func(value c_int, ...) c_int
|
|
zero c_func(...) void
|
|
bad c_func(..., value c_int) c_int
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found_ellipsis := false
|
|
for tok in stream.items {
|
|
found_ellipsis = found_ellipsis || tok.kind == .Ellipsis
|
|
}
|
|
testing.expect(t, found_ellipsis)
|
|
testing.expect(t, module.functions[0].variadic)
|
|
testing.expect_value(t, len(module.functions[0].params), 1)
|
|
testing.expect(t, module.functions[1].variadic)
|
|
testing.expect_value(t, len(module.functions[1].params), 0)
|
|
testing.expect(t, module.functions[2].variadic)
|
|
testing.expect_value(t, len(module.functions[2].params), 1)
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
testing.expect(t, strings.contains(diagnostics.items[0].message, "final parameter"))
|
|
}
|
|
|
|
@(test)
|
|
parser_treats_c_as_contextual_only_before_func :: proc(t: ^testing.T) {
|
|
text := `c :: 5
|
|
x :: c
|
|
foreign c_func() i32
|
|
broken :: c 5
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
c_symbol := symbol.intern(&symbols, "c")
|
|
for tok in stream.items {
|
|
if int(tok.span.start) < len(text) && text[int(tok.span.start):int(tok.span.end)] == "c" {
|
|
testing.expect_value(t, tok.kind, token.Kind.Identifier)
|
|
testing.expect_value(t, tok.symbol, c_symbol)
|
|
}
|
|
}
|
|
testing.expect_value(t, len(module.globals), 3)
|
|
testing.expect_value(t, module.exprs[module.globals[1].expr].name, c_symbol)
|
|
testing.expect_value(t, module.exprs[module.globals[2].expr].name, c_symbol)
|
|
testing.expect(t, module.functions[0].c_abi)
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
testing.expect(t, strings.contains(diagnostics.items[0].message, "followed by a newline"))
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_undefined_expression :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
value i32 = undefined
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found_keyword := false
|
|
for tok in stream.items {
|
|
found_keyword = found_keyword || tok.kind == .Keyword_Undefined
|
|
}
|
|
statement := module.statements[module.functions[0].body[0]]
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found_keyword)
|
|
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Undefined)
|
|
}
|
|
|
|
@(test)
|
|
pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) {
|
|
source_file := source.Source{path="test.bro", text="value :: 1 + 2 + 3\nmain func() void {}\n"}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
root := module.exprs[module.globals[0].expr]
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, root.kind, ast.Expr_Kind.Add)
|
|
testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Add)
|
|
testing.expect_value(t, module.exprs[root.right].integer, u64(3))
|
|
}
|
|
|
|
@(test)
|
|
pratt_parser_handles_prefix_negation_precedence :: proc(t: ^testing.T) {
|
|
text := `identity func(value i8) i8 { return value }
|
|
loose :: -1 + 2
|
|
grouped :: -(1 + 2)
|
|
called :: -identity(1)
|
|
chained :: --1
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
loose := module.exprs[module.globals[0].expr]
|
|
grouped := module.exprs[module.globals[1].expr]
|
|
called := module.exprs[module.globals[2].expr]
|
|
chained := module.exprs[module.globals[3].expr]
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, loose.kind, ast.Expr_Kind.Add)
|
|
testing.expect_value(t, module.exprs[loose.left].kind, ast.Expr_Kind.Negate)
|
|
testing.expect_value(t, grouped.kind, ast.Expr_Kind.Negate)
|
|
testing.expect_value(t, module.exprs[grouped.left].kind, ast.Expr_Kind.Add)
|
|
testing.expect_value(t, called.kind, ast.Expr_Kind.Negate)
|
|
testing.expect_value(t, module.exprs[called.left].kind, ast.Expr_Kind.Call)
|
|
testing.expect_value(t, chained.kind, ast.Expr_Kind.Negate)
|
|
testing.expect_value(t, module.exprs[chained.left].kind, ast.Expr_Kind.Negate)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_comptime_prefix_and_block_expressions :: proc(t: ^testing.T) {
|
|
text := `sum func(a, b int) int { return a + b }
|
|
literal :: $32
|
|
call :: $sum(1, 2) + 3
|
|
block :: ${
|
|
yield 4
|
|
}
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
literal := module.exprs[module.globals[0].expr]
|
|
call_add := module.exprs[module.globals[1].expr]
|
|
call := module.exprs[call_add.left]
|
|
block := module.exprs[module.globals[2].expr]
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, literal.kind, ast.Expr_Kind.Comptime)
|
|
testing.expect_value(t, module.exprs[literal.left].kind, ast.Expr_Kind.Integer)
|
|
testing.expect_value(t, call_add.kind, ast.Expr_Kind.Add)
|
|
testing.expect_value(t, call.kind, ast.Expr_Kind.Comptime)
|
|
testing.expect_value(t, module.exprs[call.left].kind, ast.Expr_Kind.Call)
|
|
testing.expect_value(t, block.kind, ast.Expr_Kind.Comptime)
|
|
testing.expect_value(t, len(block.body), 1)
|
|
testing.expect_value(t, module.statements[block.body[0]].kind, ast.Stmt_Kind.Yield)
|
|
}
|
|
|
|
nested_expression_source :: proc(call: bool, depth: int) -> string {
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
if call {
|
|
strings.write_string(&builder, "identity func(value i32) i32 { return value }\nvalue :: ")
|
|
for _ in 0..<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)
|
|
pipeline_emits_specialized_calling_conventions_and_checked_add :: proc(t: ^testing.T) {
|
|
text := `sum_c c_func(a, b int) int {
|
|
return a + b
|
|
}
|
|
sum_bro func(a, b int) int {
|
|
return a + b
|
|
}
|
|
main func() void {
|
|
_ = sum_c(1, 2)
|
|
_ = sum_bro(1, 2)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
second_llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(second_llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, llvm_text, second_llvm_text)
|
|
testing.expect(t, strings.contains(llvm_text, "define signext i8 @bro_c__p0__sum_c__i8__i8"))
|
|
testing.expect(t, strings.contains(llvm_text, "define internal fastcc i8 @bro__p0__sum_bro__i8__i8"))
|
|
testing.expect(t, strings.contains(llvm_text, "@llvm.sadd.with.overflow.i8"))
|
|
}
|
|
|
|
@(test)
|
|
pipeline_emits_only_referenced_foreign_declarations_with_exact_names :: proc(t: ^testing.T) {
|
|
text := `used c_func(a, b i32) i32
|
|
unused c_func() i32
|
|
bodyful c_func(value i32) i32 {
|
|
return value
|
|
}
|
|
main func() void {
|
|
_ = used(1, 2)
|
|
_ = bodyful(3)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @used(i32, i32)"))
|
|
testing.expect(t, strings.contains(llvm_text, "call i32 @used(i32 1, i32 2)"))
|
|
testing.expect(t, !strings.contains(llvm_text, "@unused("))
|
|
testing.expect(t, strings.contains(llvm_text, "define i32 @bro_c__p0__bodyful__i32"))
|
|
}
|
|
|
|
@(test)
|
|
c_primitives_remain_distinct_with_apple_silicon_representations :: proc(t: ^testing.T) {
|
|
testing.expect(t, types.C_CHAR != types.C_SCHAR)
|
|
testing.expect(t, types.C_SCHAR != types.C_UCHAR)
|
|
testing.expect(t, types.C_INT != types.I32)
|
|
testing.expect(t, types.C_ULONG != types.U64)
|
|
testing.expect_value(t, types.representation(types.C_CHAR), types.I8)
|
|
testing.expect_value(t, types.representation(types.C_SCHAR), types.I8)
|
|
testing.expect_value(t, types.representation(types.C_UCHAR), types.U8)
|
|
testing.expect_value(t, types.representation(types.C_SHORT), types.I16)
|
|
testing.expect_value(t, types.representation(types.C_USHORT), types.U16)
|
|
testing.expect_value(t, types.representation(types.C_INT), types.I32)
|
|
testing.expect_value(t, types.representation(types.C_UINT), types.U32)
|
|
testing.expect_value(t, types.representation(types.C_LONG), types.I64)
|
|
testing.expect_value(t, types.representation(types.C_ULONG), types.U64)
|
|
testing.expect_value(t, types.representation(types.C_LONGLONG), types.I64)
|
|
testing.expect_value(t, types.representation(types.C_ULONGLONG), types.U64)
|
|
testing.expect_value(t, types.representation(types.C_FLOAT), types.F32)
|
|
testing.expect_value(t, types.representation(types.C_DOUBLE), types.F64)
|
|
testing.expect_value(t, types.representation(types.C_LONGDOUBLE), types.F64)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.I8), types.C_INT)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.U16), types.C_INT)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.C_CHAR), types.C_INT)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.C_USHORT), types.C_INT)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.F32), types.C_DOUBLE)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.C_FLOAT), types.C_DOUBLE)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.U32), types.U32)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.C_DOUBLE), types.C_DOUBLE)
|
|
testing.expect_value(t, target.llvm_triple(target.DEFAULT), "arm64-apple-macosx13.0.0")
|
|
}
|
|
|
|
@(test)
|
|
interop_foundation_emits_compounds_and_narrow_c_abi_attributes :: proc(t: ^testing.T) {
|
|
text := `Point :: struct {
|
|
x i32
|
|
y i32
|
|
}
|
|
signed c_func(value c_char) c_char
|
|
unsigned c_func(value c_uchar) c_uchar
|
|
exact c_func(value u32) u32
|
|
fallback func() i32 {
|
|
return 9
|
|
}
|
|
main func() void {
|
|
c :: 1
|
|
values [2;0]mut u8 = [1, 2]
|
|
point Point :: Point{x = 3, y = 4}
|
|
maybe ?i32 = 5
|
|
_ = c
|
|
_ = values[2]
|
|
_ = (&values).ptr + 1
|
|
_ = values.len
|
|
_ = values[0..2]
|
|
_ = "hello".ptr
|
|
_ = "hello".len
|
|
_ = point.x
|
|
_ = maybe?
|
|
_ = maybe orelse 0
|
|
_ = maybe orelse fallback()
|
|
_ = signed(1)
|
|
_ = unsigned(1)
|
|
_ = exact(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "target triple = \"arm64-apple-macosx13.0.0\""))
|
|
testing.expect(t, strings.contains(llvm_text, "declare signext i8 @signed(i8 signext)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare zeroext i8 @unsigned(i8 zeroext)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @exact(i32)"))
|
|
testing.expect(t, strings.contains(llvm_text, "@bro.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\""))
|
|
testing.expect(t, strings.contains(llvm_text, "getelementptr [3 x i8]"))
|
|
testing.expect(t, strings.contains(llvm_text, "attempted to unwrap none"))
|
|
testing.expect(t, strings.contains(llvm_text, "orelse_fallback"))
|
|
testing.expect(t, strings.contains(llvm_text, "orelse_some"))
|
|
}
|
|
|
|
@(test)
|
|
string_literals_preserve_static_length_and_sentinel_through_pointer_views :: proc(t: ^testing.T) {
|
|
text := `take_sentinel_pointer func(_ [*;0]u8) void {}
|
|
take_mut_sentinel_pointer func(_ [*;0]mut u8) void {}
|
|
take_pointer func(_ *u8) void {}
|
|
take_sentinel_slice func(_ [;0]u8) void {}
|
|
take_mut_sentinel_slice func(_ [;0]mut u8) void {}
|
|
take_slice func(_ []u8) void {}
|
|
take_c_string c_func(value *c_char) c_int
|
|
take_c_sentinel c_func(value [*;0]c_char) c_int
|
|
main func() void {
|
|
text :: "hello"
|
|
values [2;0]mut u8 = [1, 2]
|
|
pointer :: &values
|
|
_ = text.len
|
|
_ = text.ptr
|
|
_ = text[0]
|
|
_ = text[1..]
|
|
_ = pointer.len
|
|
_ = pointer.ptr
|
|
_ = pointer[0]
|
|
_ = pointer[1..]
|
|
offset [*;0]u8 :: text.ptr + 1
|
|
suffix [*;0]u8 :: text[1..].ptr
|
|
middle []u8 :: text[1..3]
|
|
_ = offset
|
|
_ = suffix
|
|
_ = middle
|
|
take_sentinel_pointer(text)
|
|
take_pointer(text)
|
|
take_sentinel_slice(text)
|
|
take_slice(text)
|
|
take_mut_sentinel_pointer(pointer)
|
|
take_mut_sentinel_slice(pointer)
|
|
take_sentinel_pointer(pointer)
|
|
take_sentinel_slice(pointer)
|
|
_ = take_c_string(text)
|
|
_ = take_c_sentinel(text)
|
|
_ = take_c_string(text.ptr)
|
|
_ = take_c_sentinel(text.ptr)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
string_type := types.INVALID
|
|
decays := 0
|
|
for expr in hir_module.exprs {
|
|
if expr.kind == .String {
|
|
string_type = expr.type
|
|
}
|
|
if expr.kind == .Decay_Array_Pointer {
|
|
decays += 1
|
|
}
|
|
}
|
|
pointer, array, string_ok := types.array_pointer(string_type, &hir_module.types)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, string_ok && !pointer.mutable && array.child == types.U8 &&
|
|
array.count == 5 && array.has_sentinel && array.sentinel == 0)
|
|
testing.expect(t, decays >= 6)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\""))
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_string(ptr)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_sentinel(ptr)"))
|
|
}
|
|
|
|
@(test)
|
|
multiline_strings_join_lines_and_strip_indentation :: proc(t: ^testing.T) {
|
|
// Source written double-quoted (not a raw `...` literal) because the
|
|
// backtick is the multi-line string marker. Covers basic join, the
|
|
// trailing-newline form, a blank line in the middle, and value-on-next-line.
|
|
text := "main func() void {\n" +
|
|
"\tbasic ::\n\t\t`a\n\t\t`b\n" +
|
|
"\ttrailing ::\n\t\t`hello\n\t\t`world\n\t\t`\n" +
|
|
"\tgapped ::\n\t\t`x\n\t\t`\n\t\t`y\n" +
|
|
"\t_ = basic\n\t_ = trailing\n\t_ = gapped\n}\n"
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(ast_module.strings), 3)
|
|
testing.expect_value(t, ast_module.strings[0], "a\nb")
|
|
testing.expect_value(t, ast_module.strings[1], "hello\nworld\n")
|
|
testing.expect_value(t, ast_module.strings[2], "x\n\ny")
|
|
|
|
// A multi-line string is an ordinary string literal: @[N;0]u8.
|
|
string_type := types.INVALID
|
|
for expr in hir_module.exprs {
|
|
if expr.kind == .String {
|
|
string_type = expr.type
|
|
break
|
|
}
|
|
}
|
|
pointer, array, string_ok := types.array_pointer(string_type, &hir_module.types)
|
|
testing.expect(t, string_ok && !pointer.mutable && array.child == types.U8 &&
|
|
array.has_sentinel && array.sentinel == 0)
|
|
}
|
|
|
|
@(test)
|
|
array_pointer_and_c_string_coercion_restrictions_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := `take_c_string c_func(value *c_char) c_int
|
|
take_mut_c_string c_func(value *mut c_char) c_int
|
|
take_pointer func(value *u8) void {}
|
|
take_mut_pointer func(value *mut u8) void {}
|
|
take_slice func(value []u8) void {}
|
|
bad_sentinel func(value [*;256]u8) void {}
|
|
main func() void {
|
|
values [1;0]mut u8 = [1]
|
|
_ = values.ptr
|
|
take_pointer(values)
|
|
take_slice(values)
|
|
ordinary *u8 :: "hello"
|
|
nonzero [1;'\n']mut u8 = [1]
|
|
take_pointer("hello"[1..])
|
|
_ = take_c_string(ordinary)
|
|
_ = take_c_string((&nonzero).ptr)
|
|
_ = take_c_string(1)
|
|
take_mut_pointer("hello")
|
|
_ = take_mut_c_string("hello")
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
conversion_errors := 0
|
|
array_ptr_error := false
|
|
sentinel_error := false
|
|
for diagnostic in diagnostics.items {
|
|
conversion_errors += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0
|
|
array_ptr_error = array_ptr_error ||
|
|
strings.contains(diagnostic.message, "arrays do not expose '.ptr'")
|
|
sentinel_error = sentinel_error ||
|
|
strings.contains(diagnostic.message, "sentinel value does not fit array, slice, or pointer")
|
|
}
|
|
testing.expect_value(t, conversion_errors, 8)
|
|
testing.expect(t, array_ptr_error)
|
|
testing.expect(t, sentinel_error)
|
|
}
|
|
|
|
@(test)
|
|
immutable_pointer_and_slice_bindings_preserve_mutable_pointees :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
values [2]mut u8 = [1, 2]
|
|
pointer *mut u8 :: (&values).ptr
|
|
slice []mut u8 :: values[0..]
|
|
pointer[0] = 3
|
|
slice[1] = 4
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
many_item_pointer_slices_compile_and_run :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-pointer-slices"
|
|
main_path := "/tmp/brolang-test-pointer-slices/main.bro"
|
|
output := "/tmp/brolang-test-pointer-slices-output"
|
|
text := `main func() i32 {
|
|
values [4]mut i32 = [3, 4, 5, 6]
|
|
pointer *mut i32 :: (&values).ptr
|
|
const_pointer *i32 :: pointer
|
|
all []mut i32 :: pointer[..4]
|
|
middle []mut i32 :: pointer[1..3]
|
|
readonly []i32 :: const_pointer[..2]
|
|
|
|
if (all.len != 4) return 1
|
|
if (middle.len != 2) return 2
|
|
all[0] = 10
|
|
if (readonly[0] != 10) return 3
|
|
if (middle.ptr[0] != 4) return 4
|
|
return 0
|
|
}
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
status := compiler_core.compile_package(directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
many_item_pointer_slices_require_end_bound :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
values [2]i32 = [1, 2]
|
|
pointer *i32 :: (&values).ptr
|
|
_ = pointer[..]
|
|
_ = pointer[1..]
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
end_bound_errors := 0
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "many-item pointer slicing requires an explicit end bound") {
|
|
end_bound_errors += 1
|
|
}
|
|
}
|
|
testing.expect_value(t, end_bound_errors, 2)
|
|
}
|
|
|
|
@(test)
|
|
layout_builtins_compile_and_run :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-layout-builtins"
|
|
main_path := "/tmp/brolang-test-layout-builtins/main.bro"
|
|
output := "/tmp/brolang-test-layout-builtins-output"
|
|
text := `Point :: struct {
|
|
x i32
|
|
y u8
|
|
}
|
|
|
|
Opaque :: opaque
|
|
Color :: enum {
|
|
red
|
|
blue
|
|
}
|
|
UserID :: distinct u32
|
|
SIZE_GLOBAL :: sizeof!(i32)
|
|
|
|
needs_usize func(value usize) usize {
|
|
return value
|
|
}
|
|
|
|
buffer func($T type) [sizeof!(T)]u8 {
|
|
data [sizeof!(T)]u8 = undefined
|
|
return data
|
|
}
|
|
|
|
main func() i32 {
|
|
bytes [_]u8 :: buffer(i32)
|
|
if (needs_usize(SIZE_GLOBAL) != 4) return 1
|
|
if (bytes.len != 4) return 2
|
|
if (sizeof!([3]u8) != 3) return 3
|
|
if (sizeof!([]u8) != 16) return 4
|
|
if (alignof!([]u8) != 8) return 5
|
|
if (sizeof!(*anyopaque) != 8) return 6
|
|
if (sizeof!(?*i32) != 8) return 7
|
|
if (sizeof!(*Opaque) != 8) return 8
|
|
if (sizeof!(Color) != 2) return 9
|
|
if (alignof!(Color) != 2) return 10
|
|
if (sizeof!(Point) != 8) return 11
|
|
if (alignof!(Point) != 4) return 12
|
|
if (sizeof!(UserID) != 4) return 13
|
|
if (alignof!(UserID) != 4) return 14
|
|
return 0
|
|
}
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
status := compiler_core.compile_package(directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
integer_bound_builtins_compile_and_run :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-integer-bounds"
|
|
main_path := "/tmp/brolang-test-integer-bounds/main.bro"
|
|
output := "/tmp/brolang-test-integer-bounds-output"
|
|
text := `MAX_U64 u64 :: maxval!(u64)
|
|
|
|
maximum func($T type) T {
|
|
return maxval!(T)
|
|
}
|
|
|
|
main func() i32 {
|
|
if (minval!(i8) != -128) return 1
|
|
if (maxval!(i8) != 127) return 2
|
|
if (minval!(u8) != 0) return 3
|
|
if (maxval!(u8) != 255) return 4
|
|
if (minval!(isize) != -9223372036854775808) return 5
|
|
if (maxval!(usize) != 18446744073709551615) return 6
|
|
if (MAX_U64 != 18446744073709551615) return 7
|
|
if (maximum(u16) != 65535) return 8
|
|
if (minval!(c_int) != -2147483648) return 9
|
|
if (maxval!(c_ulong) != 18446744073709551615) return 10
|
|
return 0
|
|
}
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
status := compiler_core.compile_package(directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
integer_bound_builtins_reject_invalid_targets :: proc(t: ^testing.T) {
|
|
text := `Named :: distinct u8
|
|
Choice :: enum { one }
|
|
|
|
main func() void {
|
|
_ = minval!()
|
|
_ = maxval!(u8, u16)
|
|
_ = minval!(1)
|
|
_ = maxval!(int)
|
|
_ = maxval!(uint)
|
|
_ = maxval!(f32)
|
|
_ = maxval!(bool)
|
|
_ = maxval!(Named)
|
|
_ = maxval!(Choice)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
bad_arity := 0
|
|
bad_type := false
|
|
bad_target := 0
|
|
for diagnostic in diagnostics.items {
|
|
bad_arity += 1 if strings.contains(diagnostic.message, "expects 1 argument") else 0
|
|
bad_type = bad_type || strings.contains(diagnostic.message, "integer bound target must be a type")
|
|
bad_target += 1 if strings.contains(diagnostic.message, "integer bound target must be a concrete integer type") else 0
|
|
}
|
|
testing.expect_value(t, bad_arity, 2)
|
|
testing.expect(t, bad_type)
|
|
testing.expect_value(t, bad_target, 6)
|
|
}
|
|
|
|
@(test)
|
|
layout_builtins_reject_unsized_targets :: proc(t: ^testing.T) {
|
|
text := `Opaque :: opaque
|
|
Fn :: alias func() void
|
|
|
|
main func() void {
|
|
_ = sizeof!(void)
|
|
_ = alignof!(anyopaque)
|
|
_ = sizeof!(Fn)
|
|
_ = sizeof!(Opaque)
|
|
_ = alignof!(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
bad_layout_targets := 0
|
|
target_type_error := false
|
|
for diagnostic in diagnostics.items {
|
|
bad_layout_targets += 1 if strings.contains(diagnostic.message, "layout target must be a sized runtime value type") else 0
|
|
target_type_error = target_type_error || strings.contains(diagnostic.message, "layout target must be a type")
|
|
}
|
|
testing.expect_value(t, bad_layout_targets, 4)
|
|
testing.expect(t, target_type_error)
|
|
}
|
|
|
|
@(test)
|
|
slicing_an_array_variable_takes_its_address_implicitly :: proc(t: ^testing.T) {
|
|
// Milestone 10: `arr[a..b]` on an array variable slices without an explicit
|
|
// `&`. The slice operand must be a pointer to the array (getelementptr off a
|
|
// `ptr`), not the array value.
|
|
text := `sink func(s []i32) i32 {
|
|
return s[0]
|
|
}
|
|
main func() i32 {
|
|
arr [4]i32 = [10, 20, 30, 40]
|
|
full :: sink(arr[..])
|
|
part :: sink(arr[1..3])
|
|
return full + part
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr"))
|
|
}
|
|
|
|
@(test)
|
|
slicing_an_array_rvalue_materializes_a_temporary :: proc(t: ^testing.T) {
|
|
// The checker accepts slicing a non-location array (here, a by-value array
|
|
// return). Lowering must store it into a temporary and slice that address;
|
|
// otherwise the slice operand is an array value, which is an invalid pointer.
|
|
text := `make_arr func() [4]i32 {
|
|
return [1, 2, 3, 4]
|
|
}
|
|
sink func(s []i32) i32 {
|
|
return s[0]
|
|
}
|
|
main func() i32 {
|
|
return sink(make_arr()[0..])
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
// The materialization store of the array rvalue into its temporary.
|
|
testing.expect(t, strings.contains(llvm_text, "store [4 x i32]"))
|
|
testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr"))
|
|
}
|
|
|
|
@(test)
|
|
field_and_index_access_on_an_rvalue_aggregate_materializes_it :: proc(t: ^testing.T) {
|
|
// A by-value struct return is a temporary with no address. Reading a field,
|
|
// slicing an array field, and taking its address must spill it into a
|
|
// temporary and address that, rather than addressing the aggregate value.
|
|
text := `Box :: struct {
|
|
score i32
|
|
data [4]i32
|
|
}
|
|
make_box func() Box {
|
|
return Box { score = 7, data = [1, 2, 3, 4] }
|
|
}
|
|
sink func(s []i32) i32 {
|
|
return s[0]
|
|
}
|
|
main func() i32 {
|
|
s :: make_box().score
|
|
v :: sink(make_box().data[0..])
|
|
p :: &make_box().data
|
|
return s + v + p^[1]
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
// The rvalue Box is spilled into a stack temporary (structs render as
|
|
// %bro.type.N), then stored, then its fields are addressed off a `ptr`.
|
|
// A regression addresses the aggregate value directly, which llc rejects.
|
|
testing.expect(t, strings.contains(llvm_text, "alloca %bro.type."))
|
|
testing.expect(t, strings.contains(llvm_text, "store %bro.type."))
|
|
testing.expect(t, strings.contains(llvm_text, "getelementptr [4 x i32], ptr"))
|
|
}
|
|
|
|
@(test)
|
|
c_variadic_calls_promote_extras_and_emit_variadic_llvm :: proc(t: ^testing.T) {
|
|
text := `variadic c_func(tag c_int, ...) c_int
|
|
zero c_func(...) void
|
|
main func() void {
|
|
narrow i8 :: -2
|
|
unsigned u16 :: 3
|
|
float_value f32 :: 4.0
|
|
c_float_value c_float :: 5.0
|
|
pointer *u8 :: "ok".ptr
|
|
nullable ?*u8 :: pointer
|
|
zero(pointer)
|
|
_ = variadic(7, narrow, unsigned, float_value, c_float_value, pointer, nullable)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
promotions := 0
|
|
for expr in hir_module.exprs {
|
|
if expr.kind == .C_Vararg_Promote {
|
|
promotions += 1
|
|
}
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, promotions, 4)
|
|
found_hir_variadic := false
|
|
for function in hir_module.functions {
|
|
found_hir_variadic = found_hir_variadic || function.variadic
|
|
}
|
|
found_ir_variadic := false
|
|
for function in ir_module.functions {
|
|
found_ir_variadic = found_ir_variadic || function.variadic
|
|
}
|
|
testing.expect(t, found_hir_variadic)
|
|
testing.expect(t, found_ir_variadic)
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @variadic(i32, ...)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare void @zero(...)"))
|
|
testing.expect(t, strings.contains(llvm_text, "sext i8"))
|
|
testing.expect(t, strings.contains(llvm_text, "zext i16"))
|
|
testing.expect(t, strings.contains(llvm_text, "fpext float"))
|
|
testing.expect(t, strings.contains(llvm_text, "call void (...) @zero(ptr"))
|
|
testing.expect(t, strings.contains(llvm_text, "call i32 (i32, ...) @variadic(i32 7, i32"))
|
|
testing.expect(t, strings.contains(llvm_text, "double"))
|
|
testing.expect(t, strings.contains(llvm_text, "ptr"))
|
|
}
|
|
|
|
@(test)
|
|
c_variadic_restrictions_and_extra_argument_types_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := `Record :: c_struct {
|
|
value c_int
|
|
}
|
|
foreign c_func(...) void
|
|
requires c_func(value c_int, ...) void
|
|
native func(...) void
|
|
bodyful c_func(...) void {}
|
|
main func() void {
|
|
values [1]u8 :: [1]
|
|
record Record :: Record { value = 1 }
|
|
foreign(values)
|
|
foreign(record)
|
|
requires()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
restricted := 0
|
|
found_extra := false
|
|
found_arity := false
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "must be a bodyless 'c_func' declaration") {
|
|
restricted += 1
|
|
}
|
|
found_extra = found_extra || strings.contains(diagnostic.message, "C variadic argument must be a concrete scalar or pointer")
|
|
found_arity = found_arity || strings.contains(diagnostic.message, "expects at least 1 arguments")
|
|
}
|
|
testing.expect_value(t, restricted, 2)
|
|
testing.expect(t, found_extra)
|
|
testing.expect(t, found_arity)
|
|
}
|
|
|
|
@(test)
|
|
variadicness_is_part_of_c_function_signature_compatibility :: proc(t: ^testing.T) {
|
|
fixed := ast.Function{result=types.C_INT}
|
|
variadic := ast.Function{result=types.C_INT, variadic=true}
|
|
testing.expect(t, !checker.function_signatures_equal(fixed, variadic))
|
|
testing.expect(t, !loader.function_signatures_equal(fixed, nil, types.C_INT, true))
|
|
}
|
|
|
|
@(test)
|
|
c_structs_are_by_value_and_bodyless_c_struct_uses_opaque :: proc(t: ^testing.T) {
|
|
text := `Defined :: c_struct {
|
|
value c_int
|
|
}
|
|
Opaque :: opaque
|
|
Bodyless :: c_struct
|
|
Empty :: c_struct {}
|
|
Bad :: c_struct {
|
|
values []i32
|
|
}
|
|
read c_func(value @Defined) c_int
|
|
pass c_func(value Defined) Defined
|
|
bad_opaque c_func(value Opaque) void
|
|
main func() void {
|
|
_ = pass(Defined { value = 1 })
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_opaque := false
|
|
found_bodyless := false
|
|
found_bad_layout := false
|
|
found_empty := false
|
|
for diagnostic in diagnostics.items {
|
|
found_opaque = found_opaque || strings.contains(diagnostic.message, "cannot be passed by value")
|
|
found_bodyless = found_bodyless || strings.contains(diagnostic.message, "use 'opaque'")
|
|
found_bad_layout = found_bad_layout || strings.contains(diagnostic.message, "C-layout-compatible")
|
|
found_empty = found_empty || strings.contains(diagnostic.message, "at least one field")
|
|
}
|
|
testing.expect(t, found_opaque)
|
|
testing.expect(t, found_bodyless)
|
|
testing.expect(t, found_bad_layout)
|
|
testing.expect(t, found_empty)
|
|
}
|
|
|
|
@(test)
|
|
opaque_anyopaque_and_ptrcast_compile_and_lower :: proc(t: ^testing.T) {
|
|
text := `Handle :: opaque
|
|
take func(_ ?*mut anyopaque) void {}
|
|
use_handle func(_ ?@mut Handle) void {}
|
|
main func() void {
|
|
values [2]mut u8 = [1, 2]
|
|
raw ?*mut anyopaque = (&values).ptr
|
|
bytes ?*mut u8 = ptrcast!(u8, raw)
|
|
take(bytes)
|
|
if bytes |p| {
|
|
p[1] = 5
|
|
}
|
|
|
|
one u8 = 1
|
|
single ?@mut anyopaque = &one
|
|
typed ?@mut u8 = ptrcast!(u8, single)
|
|
if typed |p| {
|
|
p^ = 2
|
|
}
|
|
|
|
handle ?@mut Handle = none
|
|
use_handle(handle)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
hir_casts := 0
|
|
for expr in hir_module.exprs {
|
|
hir_casts += 1 if expr.kind == .Pointer_Cast else 0
|
|
}
|
|
ir_casts := 0
|
|
for function in ir_module.functions {
|
|
for instruction in function.instructions {
|
|
ir_casts += 1 if instruction.op == .Pointer_Cast else 0
|
|
}
|
|
}
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, hir_casts, 2)
|
|
testing.expect_value(t, ir_casts, 2)
|
|
}
|
|
|
|
@(test)
|
|
anyopaque_by_value_and_invalid_ptrcasts_are_rejected :: proc(t: ^testing.T) {
|
|
text := `Callback :: alias c_func() void
|
|
main func() void {
|
|
raw ?*mut anyopaque = none
|
|
value anyopaque = undefined
|
|
_ = ptrcast!(void, raw)
|
|
_ = ptrcast!(anyopaque, raw)
|
|
_ = ptrcast!(Callback, raw)
|
|
_ = ptrcast!(u8, 1)
|
|
_ = ptrcast!(1, raw)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_by_value := false
|
|
found_bad_target := false
|
|
found_bad_operand := false
|
|
found_target_type := false
|
|
for diagnostic in diagnostics.items {
|
|
found_by_value = found_by_value || strings.contains(diagnostic.message, "could not infer a concrete type")
|
|
found_bad_target = found_bad_target || strings.contains(diagnostic.message, "ptrcast! target must be a sized runtime object type")
|
|
found_bad_operand = found_bad_operand || strings.contains(diagnostic.message, "ptrcast! operand must be a pointer")
|
|
found_target_type = found_target_type || strings.contains(diagnostic.message, "ptrcast! target must be a type")
|
|
}
|
|
testing.expect(t, found_by_value)
|
|
testing.expect(t, found_bad_target)
|
|
testing.expect(t, found_bad_operand)
|
|
testing.expect(t, found_target_type)
|
|
}
|
|
|
|
@(test)
|
|
old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
_ = ptr_cast(1, 1)
|
|
_ = size_of(1)
|
|
_ = align_of(1)
|
|
_ = min_value(1)
|
|
_ = max_value(1)
|
|
_ = div_trunc(1, 1)
|
|
_ = div_floor(1, 1)
|
|
_ = div_exact(1, 1)
|
|
_ = div_ceil(1, 1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
names := [?]string{
|
|
"ptr_cast", "size_of", "align_of", "min_value", "max_value",
|
|
"div_trunc", "div_floor", "div_exact", "div_ceil",
|
|
}
|
|
found := [len(names)]bool{}
|
|
for diagnostic in diagnostics.items {
|
|
if !strings.contains(diagnostic.message, "unknown symbol") {
|
|
continue
|
|
}
|
|
for name, index in names {
|
|
found[index] = found[index] || strings.contains(diagnostic.message, name)
|
|
}
|
|
}
|
|
for value in found {
|
|
testing.expect(t, value)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
bare_intrinsic_names_are_available_to_user_functions :: proc(t: ^testing.T) {
|
|
text := `ptrcast func() i32 { return 1 }
|
|
sizeof func() i32 { return 2 }
|
|
alignof func() i32 { return 3 }
|
|
minval func() i32 { return 4 }
|
|
maxval func() i32 { return 5 }
|
|
divtrunc func() i32 { return 6 }
|
|
divfloor func() i32 { return 7 }
|
|
divexact func() i32 { return 8 }
|
|
divceil func() i32 { return 9 }
|
|
rem func() i32 { return 10 }
|
|
mod func() i32 { return 11 }
|
|
main func() i32 {
|
|
return ptrcast() + sizeof() + alignof() + minval() + maxval() +
|
|
divtrunc() + divfloor() + divexact() + divceil() + rem() + mod() - 66
|
|
}
|
|
`
|
|
directory := "/tmp/brolang-test-user-intrinsic-names"
|
|
main_path := "/tmp/brolang-test-user-intrinsic-names/main.bro"
|
|
output := "/tmp/brolang-test-user-intrinsic-names-output"
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
|
|
}
|
|
|
|
@(test)
|
|
intrinsic_call_diagnostics_are_precise :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
_ = mystery!()
|
|
_ = math.ptrcast!()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_unknown := false
|
|
found_qualified := false
|
|
for diagnostic in diagnostics.items {
|
|
found_unknown = found_unknown || diagnostic.message == "unknown intrinsic 'mystery!'"
|
|
found_qualified = found_qualified || diagnostic.message == "intrinsic calls must be unqualified"
|
|
}
|
|
testing.expect(t, found_unknown)
|
|
testing.expect(t, found_qualified)
|
|
}
|
|
|
|
@(test)
|
|
malformed_intrinsic_calls_have_targeted_parse_diagnostics :: proc(t: ^testing.T) {
|
|
cases := [?]struct {
|
|
text, message: string,
|
|
}{
|
|
{`main func() void { _ = sizeof! }`, "expected '(' after intrinsic name"},
|
|
{`callback func() void {}
|
|
main func() void { (callback)!() }
|
|
`, "intrinsic calls require a direct name"},
|
|
}
|
|
for test_case in cases {
|
|
source_file := source.Source{path="test.bro", text=test_case.text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
symbols := symbol.init_table()
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || diagnostic.message == test_case.message
|
|
}
|
|
testing.expect(t, found)
|
|
|
|
ast.destroy_module(&module)
|
|
delete(stream.items)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
aarch64_c_record_abi_classifies_fixed_parameters_and_results :: proc(t: ^testing.T) {
|
|
text := `Small :: c_struct {
|
|
left c_int
|
|
right c_int
|
|
}
|
|
Medium :: c_struct {
|
|
first c_int
|
|
second c_int
|
|
third c_int
|
|
}
|
|
Hfa :: c_struct {
|
|
x c_float
|
|
y c_float
|
|
}
|
|
Large :: c_struct {
|
|
first c_long
|
|
second c_long
|
|
third c_long
|
|
}
|
|
small c_func(value Small) Small
|
|
medium c_func(value Medium) Medium
|
|
hfa c_func(value Hfa) Hfa
|
|
large c_func(value Large) Large
|
|
main func() void {
|
|
_ = small(Small { left = 1, right = 2 })
|
|
_ = medium(Medium { first = 1, second = 2, third = 3 })
|
|
_ = hfa(Hfa { x = 1.0, y = 2.0 })
|
|
_ = large(Large { first = 1, second = 2, third = 3 })
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "declare i64 @small(i64)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare [2 x i64] @medium([2 x i64])"))
|
|
testing.expect(t, strings.contains(llvm_text, "@hfa([2 x float])"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare void @large(ptr sret("))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64"))
|
|
}
|
|
|
|
@(test)
|
|
invalid_foreign_declarations_are_eagerly_diagnosed_and_calls_trap :: proc(t: ^testing.T) {
|
|
text := `bad c_func(value int) int
|
|
native func() i32
|
|
main func() void {
|
|
_ = bad(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_parameter := false
|
|
found_result := false
|
|
found_native := false
|
|
for diagnostic in diagnostics.items {
|
|
found_parameter = found_parameter || strings.contains(diagnostic.message, "requires concrete parameter types")
|
|
found_result = found_result || strings.contains(diagnostic.message, "requires a concrete or void result type")
|
|
found_native = found_native || strings.contains(diagnostic.message, "must use 'c_func'")
|
|
}
|
|
testing.expect(t, found_parameter)
|
|
testing.expect(t, found_result)
|
|
testing.expect(t, found_native)
|
|
testing.expect(t, !strings.contains(llvm_text, "@bad("))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
|
|
}
|
|
|
|
@(test)
|
|
duplicate_foreign_symbols_across_packages_are_poisoned :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load("examples/packages/foreign_duplicate/app", &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
duplicate_count := 0
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "duplicate foreign symbol 'same'") {
|
|
duplicate_count += 1
|
|
}
|
|
}
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, duplicate_count, 2)
|
|
testing.expect(t, !strings.contains(llvm_text, "@same("))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
|
|
}
|
|
|
|
@(test)
|
|
bodyless_root_main_recovers_as_a_trap_definition :: proc(t: ^testing.T) {
|
|
text := "main c_func() i32\n"
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "main must have a body")
|
|
}
|
|
testing.expect(t, found)
|
|
testing.expect_value(t, len(ir_module.functions), 1)
|
|
testing.expect_value(t, ir_module.functions[0].implementation, ir.Implementation.Definition)
|
|
testing.expect(t, strings.contains(llvm_text, "define i32 @main()"))
|
|
testing.expect(t, !strings.contains(llvm_text, "declare i32 @main()"))
|
|
}
|
|
|
|
@(test)
|
|
milestone_33_injects_explicit_io_provider_and_runs_std_io :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load(
|
|
"examples/programs/io",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
project_root_path=".",
|
|
)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, hir_module.injected_main != hir.INVALID_FUNCTION)
|
|
testing.expect(t, hir_module.io_provider != hir.INVALID_FUNCTION)
|
|
testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main()"), 1)
|
|
testing.expect_value(t, count_substring_occurrences(llvm_text, "define internal fastcc i32 @bro__p0__main__"), 1)
|
|
testing.expect_value(t, count_substring_occurrences(llvm_text, "declare i64 @write("), 1)
|
|
|
|
output := "/tmp/brolang-test-io"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package(
|
|
"examples/programs/io",
|
|
output,
|
|
nil,
|
|
target.DEFAULT,
|
|
cimport.Options{},
|
|
".",
|
|
)
|
|
testing.expect_value(t, status, 0)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}},
|
|
context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
testing.expect_value(t, string(stdout),
|
|
"io-ok 37 {bro}\n" +
|
|
"bounds=-128/255 -32768/65535 -2147483648/4294967295 -9223372036854775808/18446744073709551615 " +
|
|
"-9223372036854775808/18446744073709551615 -128/127 -128/255 -32768/65535 -2147483648/4294967295 " +
|
|
"-9223372036854775808/18446744073709551615 -9223372036854775808/18446744073709551615 0/0 1/1 -1/1\n",
|
|
)
|
|
}
|
|
|
|
@(test)
|
|
std_io_opens_existing_files_through_the_captured_provider :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-file-io"
|
|
main_path := "/tmp/brolang-test-file-io/main.bro"
|
|
data_path := "/tmp/brolang-test-file-io/data"
|
|
output := "/tmp/brolang-test-file-io/app"
|
|
seed := "xxxxx"
|
|
text := `io :: import "@std/io"
|
|
process :: import "@std/process"
|
|
|
|
missing_fails func(system io.Io) bool {
|
|
_ = io.open(system, "/tmp/brolang-test-file-io/missing", .read_only) catch |err| {
|
|
return err == .open_failed
|
|
}
|
|
return false
|
|
}
|
|
|
|
wrong_read_fails func(file io.File) bool {
|
|
buffer [1]mut u8 = [0]
|
|
_ = io.read(io.reader(file), buffer[..]) catch |err| {
|
|
return err == .not_open_for_reading
|
|
}
|
|
return false
|
|
}
|
|
|
|
wrong_write_fails func(file io.File) bool {
|
|
_ = io.write(io.writer(file), "x") catch |err| {
|
|
return err == .not_open_for_writing
|
|
}
|
|
return false
|
|
}
|
|
|
|
main func(init process.Init) i32 {
|
|
if !missing_fails(init.io) {
|
|
return 1
|
|
}
|
|
|
|
read_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .read_only) catch |_| {
|
|
return 2
|
|
}
|
|
buffer [5]mut u8 = [0, 0, 0, 0, 0]
|
|
count usize = io.read(io.reader(read_file), buffer[..]) catch |_| {
|
|
return 4
|
|
}
|
|
if count != 5 or buffer[0] != 'x' or !wrong_write_fails(read_file) {
|
|
return 5
|
|
}
|
|
io.close(read_file) catch |_| {
|
|
return 6
|
|
}
|
|
|
|
write_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .write_only) catch |_| {
|
|
return 7
|
|
}
|
|
if !wrong_read_fails(write_file) {
|
|
return 8
|
|
}
|
|
io.write_all(io.writer(write_file), "bro") catch |_| {
|
|
return 9
|
|
}
|
|
io.close(write_file) catch |_| {
|
|
return 10
|
|
}
|
|
|
|
read_write_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .read_write) catch |_| {
|
|
return 11
|
|
}
|
|
count = io.read(io.reader(read_write_file), buffer[..]) catch |_| {
|
|
return 12
|
|
}
|
|
if count != 5 or buffer[0] != 'b' or buffer[1] != 'r' or buffer[2] != 'o' {
|
|
return 13
|
|
}
|
|
io.write_all(io.writer(read_write_file), "!") catch |_| {
|
|
return 14
|
|
}
|
|
io.close(read_write_file) catch |_| {
|
|
return 15
|
|
}
|
|
return 0
|
|
}
|
|
`
|
|
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
testing.expect(t, os.write_entire_file(data_path, transmute([]byte)seed))
|
|
|
|
status := compiler_core.compile_package(
|
|
directory,
|
|
output,
|
|
nil,
|
|
target.DEFAULT,
|
|
cimport.Options{},
|
|
".",
|
|
)
|
|
testing.expect_value(t, status, 0)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}},
|
|
context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
testing.expect_value(t, len(stdout), 0)
|
|
testing.expect_value(t, len(stderr), 0)
|
|
|
|
data, ok := os.read_entire_file(data_path)
|
|
defer delete(data)
|
|
testing.expect(t, ok)
|
|
testing.expect_value(t, string(data), "broxx!")
|
|
}
|
|
|
|
@(test)
|
|
milestone_33_rejects_non_io_and_extra_main_parameters :: proc(t: ^testing.T) {
|
|
cases := [?]string{
|
|
"main func(value i32) void {}\n",
|
|
"main func(left, right i32) void {}\n",
|
|
"main func($value i32) void {}\n",
|
|
}
|
|
for text in cases {
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
symbols := symbol.init_table()
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(
|
|
diagnostic.message,
|
|
"take no parameters or one @std/process Init",
|
|
)
|
|
}
|
|
testing.expect(t, found)
|
|
|
|
hir.destroy_module(&hir_module)
|
|
ast.destroy_module(&ast_module)
|
|
delete(stream.items)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_rejects_direct_io_main_parameter :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-direct-io-main"
|
|
main_path := "/tmp/brolang-test-direct-io-main/main.bro"
|
|
text := `io :: import "@std/io"
|
|
main func(system io.Io) void {
|
|
_ = system
|
|
}
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".")
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(
|
|
diagnostic.message,
|
|
"take no parameters or one @std/process Init",
|
|
)
|
|
}
|
|
testing.expect(t, loaded)
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_validates_process_init_and_hidden_system_provider :: proc(t: ^testing.T) {
|
|
Case :: struct {
|
|
io_source: string,
|
|
process_source: string,
|
|
message: string,
|
|
}
|
|
cases := []Case{
|
|
{
|
|
io_source=`Io :: struct { value i32 }
|
|
system func() Io { return Io {value = 0} }
|
|
`,
|
|
process_source=`io :: import "@std/io"
|
|
Init :: struct { io io.Io }
|
|
`,
|
|
message="does not provide the required 'hide system func() Io'",
|
|
},
|
|
{
|
|
io_source=`Io :: struct { value i32 }
|
|
hide system func() Io { return Io {value = 0} }
|
|
`,
|
|
process_source=`io :: import "@std/io"
|
|
Init :: struct { io io.Io, extra i32 }
|
|
`,
|
|
message="Init must be an auto-layout record containing exactly 'io io.Io'",
|
|
},
|
|
}
|
|
for test_case, index in cases {
|
|
root := fmt.tprintf("/tmp/brolang-test-process-schema-%d", index)
|
|
app := fmt.tprintf("%s/app", root)
|
|
io_dir := fmt.tprintf("%s/std/io", root)
|
|
process_dir := fmt.tprintf("%s/std/process", root)
|
|
main_source: string = `process :: import "@std/process"
|
|
main func(init process.Init) void { _ = init }
|
|
`
|
|
_ = os2.remove_all(root)
|
|
defer _ = os2.remove_all(root)
|
|
testing.expect(t, os2.make_directory_all(app) == nil)
|
|
testing.expect(t, os2.make_directory_all(io_dir) == nil)
|
|
testing.expect(t, os2.make_directory_all(process_dir) == nil)
|
|
testing.expect(t, os.write_entire_file(
|
|
fmt.tprintf("%s/main.bro", app), transmute([]byte)main_source,
|
|
))
|
|
testing.expect(t, os.write_entire_file(
|
|
fmt.tprintf("%s/io.bro", io_dir), transmute([]byte)test_case.io_source,
|
|
))
|
|
testing.expect(t, os.write_entire_file(
|
|
fmt.tprintf("%s/process.bro", process_dir), transmute([]byte)test_case.process_source,
|
|
))
|
|
|
|
sources := source.init_store()
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
symbols := symbol.init_table()
|
|
ast_module, loaded := loader.load(app, &sources, &diagnostics, &symbols, project_root_path=root)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, test_case.message)
|
|
}
|
|
testing.expect(t, loaded)
|
|
testing.expect(t, found)
|
|
hir.destroy_module(&hir_module)
|
|
ast.destroy_module(&ast_module)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
source.destroy_store(&sources)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_tuples_reflection_expand_for_and_debug_print_compile_and_run :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load(
|
|
"examples/programs/tuples", &sources, &diagnostics, &symbols, project_root_path=".",
|
|
)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, !strings.contains(llvm_text, "FormatToken"))
|
|
testing.expect(t, !strings.contains(llvm_text, "parse_format"))
|
|
testing.expect(t, !strings.contains(llvm_text, "format_field_name"))
|
|
testing.expect(t, !strings.contains(llvm_text, "FieldInfo"))
|
|
testing.expect(t, !strings.contains(llvm_text, "RecordInfo"))
|
|
|
|
output := "/tmp/brolang-test-tuples"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package(
|
|
"examples/programs/tuples", output, nil, target.DEFAULT, cimport.Options{}, ".",
|
|
)
|
|
testing.expect_value(t, status, 0)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}}, context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
testing.expect_value(t, string(stdout), "")
|
|
testing.expect_value(t, string(stderr), "hello!\ntuple=40/bro, limits=-9223372036854775808/18446744073709551615")
|
|
}
|
|
|
|
@(test)
|
|
expanded_matches_and_tag_intrinsics_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-expand"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package(
|
|
"examples/programs/expand", output, nil, target.DEFAULT, cimport.Options{}, ".",
|
|
)
|
|
testing.expect_value(t, status, 0)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}}, context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
testing.expect_value(t, string(stdout), "")
|
|
testing.expect_value(t, string(stderr), "")
|
|
}
|
|
|
|
@(test)
|
|
expanded_match_and_tag_diagnostics :: proc(t: ^testing.T) {
|
|
text := `E :: enum { a, b }
|
|
Plain :: union { value i32 }
|
|
|
|
main func() void {
|
|
inline for {1} |value| { _ = value }
|
|
|
|
n i32 = 1
|
|
match n { expand |value|: _ = value }
|
|
|
|
e E = .a
|
|
match e { expand |value, tag|: _ = value }
|
|
match e {
|
|
.a, .b: {}
|
|
expand |value|: _ = value
|
|
}
|
|
match e {
|
|
expand |value|: _ = value
|
|
.b: {}
|
|
}
|
|
match e {
|
|
else: {}
|
|
expand |value|: _ = value
|
|
}
|
|
|
|
u Plain = Plain{value = 1}
|
|
_ = tag!(u)
|
|
_ = tagname!(e)
|
|
|
|
match e { expand ||: {} }
|
|
match e { expand |a, b, c|: {} }
|
|
}
|
|
`
|
|
source_file := source.Source{path="expand_diagnostics.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_old := false
|
|
found_subject := false
|
|
found_captures := false
|
|
found_redundant := false
|
|
found_after := false
|
|
found_missing := false
|
|
found_many := false
|
|
found_tag := false
|
|
found_tagname := false
|
|
for diagnostic in diagnostics.items {
|
|
message := diagnostic.message
|
|
found_old = found_old || strings.contains(message, "'inline for' was renamed to 'expand for'")
|
|
found_subject = found_subject || strings.contains(message, "'expand' requires an enum or tagged-union")
|
|
found_captures = found_captures || strings.contains(message, "requires exactly one capture")
|
|
found_redundant = found_redundant || strings.contains(message, "redundant 'expand'")
|
|
found_after = found_after || strings.contains(message, "arms after 'expand' are unreachable") || strings.contains(message, "arms after 'else' are unreachable")
|
|
found_missing = found_missing || strings.contains(message, "expected an expand value capture")
|
|
found_many = found_many || strings.contains(message, "at most two captures")
|
|
found_tag = found_tag || strings.contains(message, "tag! requires a tagged-union value")
|
|
found_tagname = found_tagname || strings.contains(message, "tagname! requires a comptime-known enum value")
|
|
}
|
|
testing.expect(t, found_old)
|
|
testing.expect(t, found_subject)
|
|
testing.expect(t, found_captures)
|
|
testing.expect(t, found_redundant)
|
|
testing.expect(t, found_after)
|
|
testing.expect(t, found_missing)
|
|
testing.expect(t, found_many)
|
|
testing.expect(t, found_tag)
|
|
testing.expect(t, found_tagname)
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_format_errors_are_reported_at_comptime :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-format-errors"
|
|
main_path := "/tmp/brolang-test-format-errors/main.bro"
|
|
text := `io :: import "@std/io"
|
|
process :: import "@std/process"
|
|
|
|
main func(init process.Init) void {
|
|
writer io.Writer :: io.stdout(init.io)
|
|
stored :: typeinfo!(i32)
|
|
_ = stored
|
|
io.print(writer, "", 1) catch |_| {}
|
|
io.print(writer, "{s}", {1,}) catch |_| {}
|
|
io.print(writer, "{d}", {"bro",}) catch |_| {}
|
|
io.print(writer, "{d}{d}", {1,}) catch |_| {}
|
|
io.print(writer, "{d}", {1, 2}) catch |_| {}
|
|
io.print(writer, "{q}", {}) catch |_| {}
|
|
io.print(writer, "{b}", {1.5,}) catch |_| {}
|
|
io.print(writer, "{e}", {1,}) catch |_| {}
|
|
io.print(writer, "{c}", {i16(65),}) catch |_| {}
|
|
io.print(writer, "}", {}) catch |_| {}
|
|
}
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".")
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := [10]bool{}
|
|
for diagnostic in diagnostics.items {
|
|
message := diagnostic.message
|
|
found[0] = found[0] || strings.contains(message, "arguments must be a tuple")
|
|
found[1] = found[1] || strings.contains(message, "cannot implicitly convert i8 to []u8")
|
|
found[2] = found[2] || strings.contains(message, "'{d}' requires an integer or float")
|
|
found[3] = found[3] || strings.contains(message, "argument count does not match")
|
|
found[4] = found[4] || strings.contains(message, "unknown specifier")
|
|
found[5] = found[5] || strings.contains(message, "unmatched '}'")
|
|
found[6] = found[6] || strings.contains(message, "compile-time-only metadata")
|
|
found[7] = found[7] || strings.contains(message, "integer format requires an integer argument")
|
|
found[8] = found[8] || strings.contains(message, "float format requires a float argument")
|
|
found[9] = found[9] || strings.contains(message, "'{c}' requires an unsigned integer that fits in u8")
|
|
}
|
|
testing.expect(t, loaded)
|
|
for present in found {
|
|
testing.expect(t, present)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
milestone_39_stable_values_and_richer_formatting_compile_and_run :: proc(t: ^testing.T) {
|
|
stable_names: [dynamic]string
|
|
defer {
|
|
for name in stable_names {
|
|
delete(name)
|
|
}
|
|
delete(stable_names)
|
|
}
|
|
for pass := 0; pass < 2; pass += 1 {
|
|
sources := source.init_store()
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
symbols := symbol.init_table()
|
|
ast_module, loaded := loader.load(
|
|
"examples/programs/milestone_39", &sources, &diagnostics, &symbols, project_root_path=".",
|
|
)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
ir_module := lower.lower(&hir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
|
|
score_count := 0
|
|
for function in hir_module.functions {
|
|
if strings.contains(function.link_name, "bro__p0__score__ca") {
|
|
if pass == 0 {
|
|
append(&stable_names, strings.clone(function.link_name))
|
|
} else {
|
|
testing.expect_value(t, function.link_name, stable_names[score_count])
|
|
}
|
|
score_count += 1
|
|
}
|
|
}
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, score_count, 2)
|
|
testing.expect_value(t, count_substring_occurrences(llvm_text, "define internal fastcc i32 @bro__p0__read_carrier__"), 1)
|
|
testing.expect(t, !strings.contains(llvm_text, "FormatToken"))
|
|
testing.expect(t, !strings.contains(llvm_text, "parse_format"))
|
|
testing.expect(t, !strings.contains(llvm_text, "FieldInfo"))
|
|
testing.expect(t, !strings.contains(llvm_text, "EnumInfo"))
|
|
|
|
delete(llvm_text)
|
|
ir.destroy_module(&ir_module)
|
|
hir.destroy_module(&hir_module)
|
|
ast.destroy_module(&ast_module)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
source.destroy_store(&sources)
|
|
}
|
|
|
|
output := "/tmp/brolang-test-milestone-39"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package(
|
|
"examples/programs/milestone_39", output, nil, target.DEFAULT, cimport.Options{}, ".",
|
|
)
|
|
testing.expect_value(t, status, 0)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}}, context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
testing.expect_value(
|
|
t,
|
|
string(stdout),
|
|
"true -42 1.5 .running bro 2.5 1010 12 ff FF A 1.5000000000000000e+00 {} -9223372036854775808 0 inf nan 1.50000000e+00\n",
|
|
)
|
|
testing.expect_value(t, string(stderr), "debug=.idle 2a\n")
|
|
}
|
|
|
|
@(test)
|
|
milestone_39_rejects_values_without_stable_identity :: proc(t: ^testing.T) {
|
|
text := `BadUnion :: union { number i32, flag bool }
|
|
Config :: struct { value i32 }
|
|
|
|
identity func() i32 { return 1 }
|
|
reject_pointer func($value @i32) void {}
|
|
reject_function func($value @func() i32) void {}
|
|
reject_slice func($value []i32) void {}
|
|
reject_range func($value range) void {}
|
|
reject_union func($value BadUnion) void {}
|
|
reject_undefined func($value Config) void {}
|
|
|
|
stored i32 :: 1
|
|
items [2]i32 :: [1, 2]
|
|
|
|
main func() void {
|
|
reject_pointer(&stored)
|
|
reject_function(identity)
|
|
reject_slice(items[..])
|
|
reject_range(0..3)
|
|
reject_union(BadUnion {number = 1})
|
|
reject_undefined(Config {value = undefined})
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := 0
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "comptime argument has no stable comptime identity") {
|
|
found += 1
|
|
}
|
|
}
|
|
testing.expect(t, found >= 6)
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_expand_loop_control_must_be_statically_resolvable :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
total i32 = 0
|
|
expand for {1, 2} |value| {
|
|
if total == 0 {
|
|
break
|
|
}
|
|
total += value
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(
|
|
diagnostic.message,
|
|
"break or continue targeting an expand loop must be compile-time-resolvable",
|
|
)
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_comptime_undefined_aggregates_support_full_initialization :: proc(t: ^testing.T) {
|
|
text := `Token :: struct {
|
|
text []u8
|
|
count usize
|
|
}
|
|
Partial :: struct { initialized i32, text []u8 }
|
|
make_tokens func() [2]mut Token {
|
|
tokens [2]mut Token = undefined
|
|
tokens[0] = Token {text = "a", count = 1}
|
|
tokens[1].text = "bro"
|
|
tokens[1].count = 3
|
|
return tokens
|
|
}
|
|
read_initialized_sibling func() i32 {
|
|
value Partial = undefined
|
|
value.initialized = 42
|
|
return value.initialized
|
|
}
|
|
answer :: $read_initialized_sibling()
|
|
main func() i32 {
|
|
total usize = 0
|
|
expand for make_tokens() |token| {
|
|
total += token.text.len + token.count
|
|
}
|
|
if answer != 42 or total != 8 {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_comptime_undefined_values_cannot_be_observed :: proc(t: ^testing.T) {
|
|
text := `Bad :: struct { value i32, text []u8 }
|
|
read_scalar func() i32 {
|
|
value i32 = undefined
|
|
return value
|
|
}
|
|
return_partial func() Bad {
|
|
value Bad = undefined
|
|
value.value = 1
|
|
return value
|
|
}
|
|
take_bad func(value Bad) i32 {
|
|
return value.value
|
|
}
|
|
pass_partial func() i32 {
|
|
value Bad = undefined
|
|
value.value = 1
|
|
return take_bad(value)
|
|
}
|
|
bad_scalar :: $read_scalar()
|
|
bad_record :: $return_partial()
|
|
bad_argument :: $pass_partial()
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_read := false
|
|
found_return := false
|
|
found_pass := false
|
|
for diagnostic in diagnostics.items {
|
|
found_read = found_read || strings.contains(diagnostic.message, "cannot read an undefined value at comptime")
|
|
found_return = found_return || strings.contains(diagnostic.message, "comptime function returned an undefined value")
|
|
found_pass = found_pass || strings.contains(diagnostic.message, "cannot pass an undefined value at comptime")
|
|
}
|
|
testing.expect(t, found_read)
|
|
testing.expect(t, found_return)
|
|
testing.expect(t, found_pass)
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_expand_expansions_keep_distinct_call_resolutions :: proc(t: ^testing.T) {
|
|
text := `identity func($T type, value T) T {
|
|
return value
|
|
}
|
|
main func() i32 {
|
|
total i64 = 0
|
|
expand for {{i8(1), i16(2)}, {i32(3), i64(4)}} |row| {
|
|
expand for row |value| {
|
|
total += i64(identity(value))
|
|
}
|
|
}
|
|
return i32(total - 10)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
identity_symbol := symbol.intern(&symbols, "identity")
|
|
specializations := 0
|
|
for function in hir_module.functions {
|
|
specializations += 1 if function.name == identity_symbol else 0
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, specializations, 4)
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_expand_control_prunes_inference_after_static_exit :: proc(t: ^testing.T) {
|
|
text := `take_i8 func(value i8) void { _ = value }
|
|
main func() void {
|
|
expand for {i8(1), "skip"} |value, index| {
|
|
if index == 1 {
|
|
continue
|
|
}
|
|
take_i8(value)
|
|
}
|
|
expand for {i8(1), "stop"} |value, index| {
|
|
if index == 1 {
|
|
break
|
|
}
|
|
take_i8(value)
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
milestone_37_expand_match_specialization_prunes_unselected_arms :: proc(t: ^testing.T) {
|
|
text := `Kind :: enum { integer, string, stop }
|
|
IntToken :: struct { kind Kind, value i8 }
|
|
StringToken :: struct { kind Kind, value []u8 }
|
|
StopToken :: struct { kind Kind, value bool }
|
|
take_i8 func(value i8) void { _ = value }
|
|
main func() void {
|
|
expand for {
|
|
IntToken {kind = .integer, value = 1},
|
|
StringToken {kind = .string, value = "ok"},
|
|
StopToken {kind = .stop, value = false},
|
|
} |token| {
|
|
match token.kind {
|
|
.integer: take_i8(token.value)
|
|
.string: {
|
|
_ = token.value.len
|
|
continue
|
|
}
|
|
.stop: break
|
|
}
|
|
}
|
|
expand for {i8(2), "skip", "stop"} |value, index| {
|
|
match index {
|
|
0: {}
|
|
1..=1, 7: continue
|
|
else: break
|
|
}
|
|
take_i8(value)
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
milestone_33_rejects_an_incompatible_runtime_write_declaration :: proc(t: ^testing.T) {
|
|
text := `write c_func(_ c_int, _ c_int, _ c_ulong) c_long
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(
|
|
diagnostic.message,
|
|
"external C function 'write' conflicts with the compiler runtime declaration",
|
|
)
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) {
|
|
text := `return_i16 func() i16 {
|
|
return 1 + 2
|
|
}
|
|
take_i16 func(value i16) i16 {
|
|
return value
|
|
}
|
|
take_int func(value int) int {
|
|
return value
|
|
}
|
|
main func() void {
|
|
local i16 :: 1 + 2
|
|
_ = local
|
|
_ = 100 + (20 + 8)
|
|
_ = return_i16()
|
|
_ = take_i16(1 + 2)
|
|
_ = take_int(127 + 1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__take_int__i16"))
|
|
for function in ir_module.functions {
|
|
for instruction in function.instructions {
|
|
testing.expect(t, instruction.op != ir.Opcode.Add_Checked)
|
|
}
|
|
}
|
|
|
|
for function in hir_module.functions {
|
|
for statement_id in function.body {
|
|
statement := hir_module.statements[statement_id]
|
|
if statement.expr < 0 {
|
|
continue
|
|
}
|
|
expr := hir_module.exprs[statement.expr]
|
|
if expr.kind == .Integer {
|
|
testing.expect(t, types.equal(expr.type, types.I16))
|
|
}
|
|
if expr.kind == .Call && symbol.resolve(&symbols, function.name) == "main" && len(expr.args) > 0 {
|
|
arg := hir_module.exprs[expr.args[0]]
|
|
testing.expect_value(t, arg.kind, hir.Expr_Kind.Integer)
|
|
testing.expect(t, types.equal(arg.type, types.I16))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
runtime_arithmetic_does_not_inherit_result_context :: proc(t: ^testing.T) {
|
|
text := `widen_after_add func(value i8) i16 {
|
|
return value + 1
|
|
}
|
|
main func() void {
|
|
_ = widen_after_add(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
found := false
|
|
for function in hir_module.functions {
|
|
if symbol.resolve(&symbols, function.name) != "widen_after_add" {
|
|
continue
|
|
}
|
|
found = true
|
|
statement := hir_module.statements[function.body[0]]
|
|
widen := hir_module.exprs[statement.expr]
|
|
add := hir_module.exprs[widen.left]
|
|
testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen)
|
|
testing.expect(t, types.equal(widen.type, types.I16))
|
|
testing.expect_value(t, add.kind, hir.Expr_Kind.Add)
|
|
testing.expect(t, types.equal(add.type, types.I8))
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
parser_recovers_after_invalid_tokens :: proc(t: ^testing.T) {
|
|
text := `broken @ declaration
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect(t, len(diagnostics.items) > 0)
|
|
testing.expect_value(t, len(module.functions), 1)
|
|
testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "main")
|
|
}
|
|
|
|
@(test)
|
|
return_sink_and_unconsumed_values_have_distinct_hir :: proc(t: ^testing.T) {
|
|
text := `give func() i8 {
|
|
return 1
|
|
}
|
|
done func() void {
|
|
return
|
|
}
|
|
main func() void {
|
|
done()
|
|
_ = give()
|
|
give()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
done_id, main_id := -1, -1
|
|
for function, id in hir_module.functions {
|
|
if symbol.resolve(&symbols, function.name) == "done" {
|
|
done_id = id
|
|
} else if symbol.resolve(&symbols, function.name) == "main" {
|
|
main_id = id
|
|
}
|
|
}
|
|
testing.expect(t, done_id >= 0)
|
|
testing.expect(t, main_id >= 0)
|
|
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].kind, hir.Stmt_Kind.Return)
|
|
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].expr, hir.INVALID_EXPR)
|
|
main := hir_module.functions[main_id]
|
|
testing.expect_value(t, hir_module.statements[main.body[0]].kind, hir.Stmt_Kind.Expression)
|
|
testing.expect_value(t, hir_module.statements[main.body[1]].kind, hir.Stmt_Kind.Sink)
|
|
testing.expect_value(t, hir_module.statements[main.body[2]].kind, hir.Stmt_Kind.Trap)
|
|
}
|
|
|
|
@(test)
|
|
bare_returns_and_strict_yields :: proc(t: ^testing.T) {
|
|
text := `Failure :: enum { bad }
|
|
noop func() void {}
|
|
newline_return func() void {
|
|
return
|
|
}
|
|
inline_return func() void { return }
|
|
fallible_return func() void ! Failure { return }
|
|
split_return func() i32 {
|
|
return
|
|
1
|
|
}
|
|
old_return func() void { return _ }
|
|
missing_yield func() i32 {
|
|
value :: {
|
|
yield
|
|
1
|
|
}
|
|
return value
|
|
}
|
|
missing_labeled_yield func() i32 {
|
|
value :: block: {
|
|
yield :block
|
|
}
|
|
return value
|
|
}
|
|
void_yield func() i32 {
|
|
value :: { yield noop() }
|
|
return value
|
|
}
|
|
sink_yield func() i32 {
|
|
value :: { yield _ }
|
|
return value
|
|
}
|
|
void_context func() void {
|
|
fallible_return() catch |_| { yield 1 }
|
|
}
|
|
bad_comptime :: ${ yield noop() }
|
|
main func() void {
|
|
newline_return()
|
|
inline_return()
|
|
fallible_return() catch |_| {}
|
|
_ = split_return()
|
|
old_return()
|
|
_ = missing_yield()
|
|
_ = missing_labeled_yield()
|
|
_ = void_yield()
|
|
_ = sink_yield()
|
|
void_context()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, ast_module.statements[ast_module.functions[1].body[0]].expr, ast.INVALID_EXPR)
|
|
testing.expect_value(t, ast_module.statements[ast_module.functions[2].body[0]].expr, ast.INVALID_EXPR)
|
|
testing.expect_value(t, ast_module.statements[ast_module.functions[3].body[0]].expr, ast.INVALID_EXPR)
|
|
testing.expect_value(t, len(ast_module.functions[4].body), 2)
|
|
|
|
missing_value := false
|
|
non_void_function := false
|
|
void_yields := 0
|
|
sink_read := false
|
|
void_context := false
|
|
for diagnostic in diagnostics.items {
|
|
missing_value = missing_value || strings.contains(diagnostic.message, "'yield' must produce a value")
|
|
non_void_function = non_void_function || strings.contains(diagnostic.message, "non-void function must return a value")
|
|
if strings.contains(diagnostic.message, "'yield' expression must produce a non-void value") {
|
|
void_yields += 1
|
|
}
|
|
sink_read = sink_read || strings.contains(diagnostic.message, "'_' is a write-only sink and cannot be read")
|
|
void_context = void_context || strings.contains(diagnostic.message, "void value context must fall through instead of yielding")
|
|
}
|
|
testing.expect(t, missing_value)
|
|
testing.expect(t, non_void_function)
|
|
testing.expect(t, void_yields >= 2)
|
|
testing.expect(t, sink_read)
|
|
testing.expect(t, void_context)
|
|
}
|
|
|
|
@(test)
|
|
unused_locals_and_params_warn_without_traps :: proc(t: ^testing.T) {
|
|
text := `warn_only func(value i32, unused i32) i32 {
|
|
local i32 = 1
|
|
write_only i32 = 2
|
|
write_only = 3
|
|
consumed i32 = value
|
|
_ = consumed
|
|
return value
|
|
}
|
|
main func() void {
|
|
_ = warn_only(1, 2)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_unused_param := false
|
|
found_unused_local := false
|
|
found_write_only := false
|
|
warning_count := 0
|
|
error_count := 0
|
|
for diagnostic in diagnostics.items {
|
|
if diagnostic.severity == source.Severity.Warning {
|
|
warning_count += 1
|
|
} else {
|
|
error_count += 1
|
|
}
|
|
found_unused_param = found_unused_param || strings.contains(diagnostic.message, "unused parameter 'unused'")
|
|
found_unused_local = found_unused_local || strings.contains(diagnostic.message, "unused local 'local'")
|
|
found_write_only = found_write_only || strings.contains(diagnostic.message, "unused local 'write_only'")
|
|
testing.expect(t, !strings.contains(diagnostic.message, "unused local 'consumed'"))
|
|
testing.expect(t, !strings.contains(diagnostic.message, "unused parameter 'value'"))
|
|
}
|
|
testing.expect_value(t, warning_count, 3)
|
|
testing.expect_value(t, error_count, 0)
|
|
testing.expect(t, found_unused_param)
|
|
testing.expect(t, found_unused_local)
|
|
testing.expect(t, found_write_only)
|
|
|
|
found_function := false
|
|
for function in hir_module.functions {
|
|
if symbol.resolve(&symbols, function.name) != "warn_only" {
|
|
continue
|
|
}
|
|
found_function = true
|
|
testing.expect(t, !function.problematic)
|
|
for stmt_id in function.body {
|
|
testing.expect(t, hir_module.statements[stmt_id].kind != hir.Stmt_Kind.Trap)
|
|
}
|
|
}
|
|
testing.expect(t, found_function)
|
|
}
|
|
|
|
@(test)
|
|
generic_parameter_usage_is_source_based :: proc(t: ^testing.T) {
|
|
text := `choose func($N usize, used, unused i32) i32 {
|
|
if N > 0 {
|
|
return used
|
|
}
|
|
return 0
|
|
}
|
|
main func() void {
|
|
_ = choose(0, 1, 2)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
testing.expect_value(t, diagnostics.items[0].severity, source.Severity.Warning)
|
|
testing.expect(t, strings.contains(diagnostics.items[0].message, "unused parameter 'unused'"))
|
|
testing.expect(t, !strings.contains(diagnostics.items[0].message, "unused parameter 'used'"))
|
|
}
|
|
|
|
@(test)
|
|
recursive_specialization_reaches_a_fixed_point :: proc(t: ^testing.T) {
|
|
text := `a func(value int) i32 {
|
|
return b(value)
|
|
}
|
|
b func(value int) i32 {
|
|
return a(value)
|
|
}
|
|
main func() void {
|
|
_ = a(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 3)
|
|
}
|
|
|
|
@(test)
|
|
stale_specializations_are_pruned_after_inference :: proc(t: ^testing.T) {
|
|
text := `derived :: identity(make())
|
|
wide :: delayed()
|
|
identity func(value int) int {
|
|
return value
|
|
}
|
|
make func() int {
|
|
return wide
|
|
return 1
|
|
}
|
|
delayed func() int {
|
|
return 128
|
|
}
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 4)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__identity__i16"))
|
|
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity__i8"))
|
|
}
|
|
|
|
@(test)
|
|
eager_global_calls_root_specializations :: proc(t: ^testing.T) {
|
|
text := `make func() i32 {
|
|
return 7
|
|
}
|
|
unused_native func() i32 {
|
|
return 9
|
|
}
|
|
unused_foreign c_func() i32
|
|
value i32 :: make()
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 2)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__make("))
|
|
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__unused_native("))
|
|
testing.expect(t, !strings.contains(llvm_text, "@unused_foreign("))
|
|
}
|
|
|
|
@(test)
|
|
malformed_generic_calls_do_not_retain_specializations :: proc(t: ^testing.T) {
|
|
text := `identity func(value int) int {
|
|
return value
|
|
}
|
|
bad :: identity(1, 2)
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_arity := false
|
|
for diagnostic in diagnostics.items {
|
|
found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 arguments, got 2")
|
|
}
|
|
testing.expect(t, found_arity)
|
|
testing.expect_value(t, len(hir_module.functions), 1)
|
|
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity"))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
|
|
}
|
|
|
|
@(test)
|
|
long_generic_call_chain_reaches_a_fixed_point :: proc(t: ^testing.T) {
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
for index in 0 ..< 70 {
|
|
fmt.sbprintf(&builder, "fn%d func(value int) int ", index)
|
|
strings.write_string(&builder, "{ return ")
|
|
if index == 69 {
|
|
strings.write_string(&builder, "value")
|
|
} else {
|
|
fmt.sbprintf(&builder, "fn%d(value)", index+1)
|
|
}
|
|
strings.write_string(&builder, " }\n")
|
|
}
|
|
strings.write_string(&builder, "main func() i32 { return fn0(1) }\n")
|
|
|
|
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 71)
|
|
}
|
|
|
|
@(test)
|
|
globals_and_generic_results_reach_a_shared_fixed_point :: proc(t: ^testing.T) {
|
|
text := `derived :: identity(base)
|
|
base :: make()
|
|
identity func(value int) int {
|
|
return value
|
|
}
|
|
make func() int {
|
|
return 1
|
|
}
|
|
main func() i32 {
|
|
return derived
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 3)
|
|
testing.expect(t, types.equal(hir_module.globals[0].type, types.I32))
|
|
testing.expect(t, types.equal(hir_module.globals[1].type, types.I8))
|
|
}
|
|
|
|
@(test)
|
|
comptime_value_params_specialize_by_value_and_omit_runtime_args :: proc(t: ^testing.T) {
|
|
text := `make_array func($N usize) [N]u8 {
|
|
data [N]u8 = undefined
|
|
return data
|
|
}
|
|
main func() void {
|
|
four [_]u8 :: make_array(4)
|
|
eight [_]u8 :: make_array(8)
|
|
again [_]u8 :: make_array(4)
|
|
literal [_]u8 :: [1, 2, 3, 4]
|
|
_ = four
|
|
_ = eight
|
|
_ = again
|
|
_ = literal
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
make_specs := 0
|
|
for function in hir_module.functions {
|
|
if symbol.resolve(&symbols, function.name) == "make_array" {
|
|
make_specs += 1
|
|
testing.expect_value(t, len(function.params), 0)
|
|
}
|
|
}
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, make_specs, 2)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__make_array__cv4"))
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__make_array__cv8"))
|
|
}
|
|
|
|
@(test)
|
|
comptime_value_params_diagnose_invalid_uses :: proc(t: ^testing.T) {
|
|
text := `make func($N usize) i32 {
|
|
return N
|
|
}
|
|
tiny func($N u8) i32 {
|
|
return N
|
|
}
|
|
good_bool func($T bool) void {}
|
|
bad_use func($N usize) void {
|
|
N = 1
|
|
_ = &N
|
|
}
|
|
main func() void {
|
|
good_bool(true)
|
|
x usize = 4
|
|
_ = make(x)
|
|
_ = make()
|
|
_ = make(1, 2)
|
|
_ = make(-1)
|
|
_ = tiny(300)
|
|
bad_use(4)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_runtime_arg := false
|
|
found_missing := false
|
|
found_extra := false
|
|
found_negative := false
|
|
found_range := false
|
|
found_assignment := false
|
|
found_address := false
|
|
for diagnostic in diagnostics.items {
|
|
message := diagnostic.message
|
|
found_runtime_arg = found_runtime_arg || strings.contains(message, "must be a compile-time integer expression")
|
|
found_missing = found_missing || strings.contains(message, "cannot infer comptime parameter 'N'")
|
|
found_extra = found_extra || strings.contains(message, "has no unique complete argument mapping")
|
|
found_negative = found_negative || strings.contains(message, "integer constant -1 does not fit in usize")
|
|
found_range = found_range || strings.contains(message, "integer constant 300 does not fit in u8")
|
|
found_assignment = found_assignment || strings.contains(message, "cannot assign comptime parameter 'N'")
|
|
found_address = found_address || strings.contains(message, "'&' requires an addressable location")
|
|
}
|
|
|
|
testing.expect(t, found_runtime_arg)
|
|
testing.expect(t, found_missing)
|
|
testing.expect(t, found_extra)
|
|
testing.expect(t, found_negative)
|
|
testing.expect(t, found_range)
|
|
testing.expect(t, found_assignment)
|
|
testing.expect(t, found_address)
|
|
}
|
|
|
|
@(test)
|
|
inferred_comptime_params_diagnose_ambiguous_calls :: proc(t: ^testing.T) {
|
|
text := `Ignored func($T type) type {
|
|
return i32
|
|
}
|
|
Box func($T type) type {
|
|
return struct { value T }
|
|
}
|
|
BoxAlias func($T type) type {
|
|
return Box(T)
|
|
}
|
|
conflict func($T type, left, right T) T { return left }
|
|
unknown func($T type) T { value T = undefined; return value }
|
|
partial func($T type, $N usize, value T) T { return value }
|
|
use_ignored func($T type, value Ignored(T)) i32 { return value }
|
|
use_alias func($T type, value BoxAlias(T)) T { return value.value }
|
|
mapping_fail func($A usize, value i32, $B usize) void {}
|
|
main func() void {
|
|
a i32 :: 1
|
|
b u32 :: 2
|
|
_ = conflict(a, b)
|
|
_ = unknown()
|
|
_ = partial(i32, a)
|
|
_ = use_ignored(a)
|
|
box Box(i32) :: Box(i32) { value = 1 }
|
|
_ = use_alias(box)
|
|
mapping_fail(true, false)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_conflict := false
|
|
found_unknown := false
|
|
found_partial := false
|
|
found_candidate_failures := false
|
|
unrecoverable := 0
|
|
for diagnostic in diagnostics.items {
|
|
message := diagnostic.message
|
|
found_conflict = found_conflict || strings.contains(message, "conflicting inference for comptime parameter 'T': i32 and u32")
|
|
found_unknown = found_unknown || strings.contains(message, "cannot infer comptime parameter 'T'")
|
|
found_partial = found_partial || strings.contains(message, "cannot infer comptime parameter 'N'")
|
|
found_candidate_failures = found_candidate_failures ||
|
|
strings.contains(message, "candidate 1: cannot infer comptime parameter 'B'") &&
|
|
strings.contains(message, "candidate 2: cannot infer comptime parameter 'A'")
|
|
if strings.contains(message, "cannot infer comptime parameter 'T'") {
|
|
unrecoverable += 1
|
|
}
|
|
}
|
|
testing.expect(t, found_conflict)
|
|
testing.expect(t, found_unknown)
|
|
testing.expect(t, found_partial)
|
|
testing.expect(t, found_candidate_failures)
|
|
testing.expect(t, unrecoverable >= 3)
|
|
}
|
|
|
|
@(test)
|
|
comptime_params_may_be_interleaved_and_are_erased_from_the_abi :: proc(t: ^testing.T) {
|
|
text := `valid func($T type, $N usize, value T) [N]T {
|
|
result [N]T = undefined
|
|
_ = value
|
|
return result
|
|
}
|
|
runtime_first func(value T, $T type, $N usize) T { return value }
|
|
split func($T type, value T, $N usize) [N]T {
|
|
result [N]T = undefined
|
|
_ = value
|
|
return result
|
|
}
|
|
from_result func($T type) T {
|
|
value T = undefined
|
|
return value
|
|
}
|
|
choose_mapping func($A usize, value i32, $B usize) [A]u8 {
|
|
result [A]u8 = undefined
|
|
_ = value
|
|
_ = B
|
|
return result
|
|
}
|
|
main func() void {
|
|
_ = runtime_first(42, i32, 4)
|
|
_ = runtime_first(42, _, 4)
|
|
_ = split(i32, 42, 4)
|
|
_ = split(42, 4)
|
|
value i32 = from_result()
|
|
chosen [1]u8 :: choose_mapping(7, 2)
|
|
_ = value
|
|
_ = chosen
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
for function in hir_module.functions {
|
|
name := symbol.resolve(&symbols, function.name)
|
|
if name == "runtime_first" || name == "split" {
|
|
testing.expect_value(t, len(function.params), 1)
|
|
} else if name == "from_result" {
|
|
testing.expect_value(t, len(function.params), 0)
|
|
} else if name == "choose_mapping" {
|
|
testing.expect_value(t, len(function.params), 1)
|
|
}
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
comptime_type_params_specialize_by_type_and_omit_runtime_args :: proc(t: ^testing.T) {
|
|
text := `Point :: struct {
|
|
x i32
|
|
}
|
|
id func($T type, value T) T {
|
|
return value
|
|
}
|
|
zero func($T type) T {
|
|
value T = undefined
|
|
return value
|
|
}
|
|
buffer func($T type, $N usize, _ T) [N]T {
|
|
data [N]T = undefined
|
|
return data
|
|
}
|
|
main func() void {
|
|
a i32 :: 42
|
|
b u8 :: 7
|
|
p Point :: Point { x = 9 }
|
|
_ = id(i32, a)
|
|
_ = id(u8, b)
|
|
_ = id(Point, p)
|
|
_ = zero(i32)
|
|
_ = buffer(u8, 4, b)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
id_specs := 0
|
|
zero_specs := 0
|
|
buffer_specs := 0
|
|
for function in hir_module.functions {
|
|
name := symbol.resolve(&symbols, function.name)
|
|
if name == "id" {
|
|
id_specs += 1
|
|
testing.expect_value(t, len(function.params), 1)
|
|
} else if name == "zero" {
|
|
zero_specs += 1
|
|
testing.expect_value(t, len(function.params), 0)
|
|
} else if name == "buffer" {
|
|
buffer_specs += 1
|
|
testing.expect_value(t, len(function.params), 1)
|
|
}
|
|
}
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, id_specs, 3)
|
|
testing.expect_value(t, zero_specs, 1)
|
|
testing.expect_value(t, buffer_specs, 1)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__id__i32__cti32"))
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__zero__cti32"))
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__buffer__u8__ctu8__cv4"))
|
|
}
|
|
|
|
@(test)
|
|
comptime_type_params_diagnose_invalid_uses :: proc(t: ^testing.T) {
|
|
text := `id func($T type, value T) T {
|
|
return value
|
|
}
|
|
bad_c c_func($T type) void
|
|
bad_value func($T type) void {
|
|
_ = T
|
|
}
|
|
bad_assign func($T type) void {
|
|
T = 1
|
|
}
|
|
main func() void {
|
|
x i32 = 1
|
|
_ = id(x, x)
|
|
bad_value(i32)
|
|
bad_assign(i32)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_arg := false
|
|
found_c_func := false
|
|
found_value := false
|
|
found_assign := false
|
|
for diagnostic in diagnostics.items {
|
|
message := diagnostic.message
|
|
found_arg = found_arg || strings.contains(message, "argument for comptime type parameter 'T' must be a type")
|
|
found_c_func = found_c_func || strings.contains(message, "comptime parameters require 'func', not 'c_func'")
|
|
found_value = found_value || strings.contains(message, "type parameter 'T' is not a runtime value")
|
|
found_assign = found_assign || strings.contains(message, "cannot assign comptime parameter 'T'")
|
|
}
|
|
|
|
testing.expect(t, found_arg)
|
|
testing.expect(t, found_c_func)
|
|
testing.expect(t, found_value)
|
|
testing.expect(t, found_assign)
|
|
}
|
|
|
|
@(test)
|
|
comptime_expression_forces_integer_evaluation :: proc(t: ^testing.T) {
|
|
text := `sum func(a, b int) int {
|
|
return a + b
|
|
}
|
|
max func(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
nested func(value int) int {
|
|
two :: 2
|
|
return sum(value, two)
|
|
}
|
|
make_array func($N usize) [N]u8 {
|
|
data [N]u8 = undefined
|
|
return data
|
|
}
|
|
forced :: $sum(1, 2)
|
|
main func() i32 {
|
|
value i32 :: $sum(20, 22)
|
|
choice i32 :: $max(9, 3)
|
|
blocked i32 :: ${
|
|
local :: 5
|
|
yield sum(local, 6)
|
|
}
|
|
bytes [_]u8 :: make_array($nested(2))
|
|
if forced != 3 {
|
|
return 1
|
|
}
|
|
if value != 42 {
|
|
return 2
|
|
}
|
|
if choice != 9 {
|
|
return 3
|
|
}
|
|
if blocked != 11 {
|
|
return 4
|
|
}
|
|
if bytes.len != 4 {
|
|
return 5
|
|
}
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i8 3"))
|
|
}
|
|
|
|
@(test)
|
|
comptime_expression_diagnoses_runtime_only_and_quota :: proc(t: ^testing.T) {
|
|
text := `native c_func() i32
|
|
spin func() i32 {
|
|
while true {
|
|
}
|
|
return 0
|
|
}
|
|
missing func() i32 {
|
|
if true {
|
|
}
|
|
}
|
|
GLOBAL :: 1
|
|
main func() void {
|
|
runtime i32 = 1
|
|
_ = $runtime
|
|
_ = $native()
|
|
_ = ${
|
|
callback :: native
|
|
yield callback()
|
|
}
|
|
_ = $&GLOBAL
|
|
_ = $spin()
|
|
_ = $missing()
|
|
_ = ${
|
|
value :: 1
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_runtime := false
|
|
runtime_only_count := 0
|
|
found_pointer := false
|
|
found_quota := false
|
|
found_missing := false
|
|
found_yield := false
|
|
for diagnostic in diagnostics.items {
|
|
message := diagnostic.message
|
|
found_runtime = found_runtime || strings.contains(message, "unresolved comptime value 'runtime'")
|
|
runtime_only_count += 1 if strings.contains(message, "runtime-only") else 0
|
|
found_pointer = found_pointer || strings.contains(message, "comptime storage pointers and slices cannot materialize as runtime memory")
|
|
found_quota = found_quota || strings.contains(message, "comptime evaluation exceeded the step quota")
|
|
found_missing = found_missing || strings.contains(message, "did not return a value")
|
|
found_yield = found_yield || strings.contains(message, "comptime block must yield a value")
|
|
}
|
|
testing.expect(t, found_runtime)
|
|
testing.expect(t, runtime_only_count >= 2)
|
|
testing.expect(t, found_pointer)
|
|
testing.expect(t, found_quota)
|
|
testing.expect(t, found_missing)
|
|
testing.expect(t, found_yield)
|
|
}
|
|
|
|
@(test)
|
|
native_function_pointer_type_restrictions_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
callback @func(...) void = undefined
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_variadic := false
|
|
for diagnostic in diagnostics.items {
|
|
found_variadic =
|
|
found_variadic ||
|
|
strings.contains(diagnostic.message, "native function pointer types do not support variadic parameters")
|
|
}
|
|
testing.expect(t, found_variadic)
|
|
}
|
|
|
|
@(test)
|
|
unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) {
|
|
text := `broken func(value, value i8, nope void) void {}
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_duplicate := false
|
|
found_void := false
|
|
for diagnostic in diagnostics.items {
|
|
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate parameter 'value'")
|
|
found_void = found_void || strings.contains(diagnostic.message, "void is only valid as a function result type")
|
|
}
|
|
testing.expect(t, found_duplicate)
|
|
testing.expect(t, found_void)
|
|
}
|
|
|
|
run_command_success :: proc(command: []string) -> bool {
|
|
state, stdout, stderr, err := os2.process_exec(
|
|
os2.Process_Desc{command=command},
|
|
context.allocator,
|
|
)
|
|
delete(stdout)
|
|
delete(stderr)
|
|
return err == nil && state.exit_code == 0
|
|
}
|
|
|
|
prepare_native_object :: proc(output: string) -> bool {
|
|
local_cache := fmt.aprintf("ZIG_LOCAL_CACHE_DIR=%s-zig-cache", output)
|
|
defer delete(local_cache)
|
|
global_cache := fmt.aprintf("ZIG_GLOBAL_CACHE_DIR=%s-zig-global-cache", output)
|
|
defer delete(global_cache)
|
|
return run_command_success([]string{
|
|
"/usr/bin/env",
|
|
local_cache,
|
|
global_cache,
|
|
"zig",
|
|
"cc",
|
|
"-c",
|
|
"examples/interop/manual/native.c",
|
|
"-o",
|
|
output,
|
|
})
|
|
}
|
|
|
|
prepare_native_archive :: proc(object, archive: string) -> bool {
|
|
return run_command_success([]string{
|
|
"/usr/bin/ar",
|
|
"-rcs",
|
|
archive,
|
|
object,
|
|
})
|
|
}
|
|
|
|
run_executable :: proc(path: string) -> os2.Process_State {
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{path}},
|
|
context.allocator,
|
|
)
|
|
delete(stdout)
|
|
delete(stderr)
|
|
return state
|
|
}
|
|
|
|
@(test)
|
|
valid_program_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-valid"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/prototype", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
unused_local_warnings_return_status_one_but_do_not_trap :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-unused-locals"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/unused_locals", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 7)
|
|
}
|
|
|
|
@(test)
|
|
build_command_is_recognized :: proc(t: ^testing.T) {
|
|
testing.expect(t, is_build_command("build"))
|
|
testing.expect(t, !is_build_command("translate-c"))
|
|
testing.expect(t, !is_build_command("--build"))
|
|
}
|
|
|
|
@(test)
|
|
version_command_is_recognized :: proc(t: ^testing.T) {
|
|
testing.expect(t, is_version_command("version"))
|
|
testing.expect(t, !is_version_command("--version"))
|
|
testing.expect(t, len(BROLANG_VERSION) > 0)
|
|
}
|
|
|
|
@(test)
|
|
build_subcommand_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "examples/build/hello/build/hello"
|
|
defer _ = os2.remove_all("examples/build/hello/build")
|
|
status := compiler_core.run_build("examples/build/hello")
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
status = compiler_core.run_build("examples/build/hello")
|
|
testing.expect_value(t, status, 0)
|
|
testing.expect(t, os.exists(output))
|
|
}
|
|
|
|
@(test)
|
|
build_subcommand_links_c_source_via_list_field :: proc(t: ^testing.T) {
|
|
output := "examples/build/manual/build/manual_build"
|
|
defer _ = os2.remove_all("examples/build/manual/build")
|
|
status := compiler_core.run_build("examples/build/manual")
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
new_project_creates_layout_and_builds :: proc(t: ^testing.T) {
|
|
root := "/tmp/brolang-test-new-project"
|
|
output := "/tmp/brolang-test-new-project/build/brolang-test-new-project"
|
|
_ = os2.remove_all(root)
|
|
defer _ = os2.remove_all(root)
|
|
|
|
testing.expect_value(t, run_new_project(root), 0)
|
|
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/source"))
|
|
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/std"))
|
|
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/ffi"))
|
|
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/vendor"))
|
|
testing.expect(t, os.exists("/tmp/brolang-test-new-project/build.bro"))
|
|
testing.expect(t, os.exists("/tmp/brolang-test-new-project/source/main.bro"))
|
|
|
|
status := compiler_core.run_build(root)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
init_project_creates_missing_layout_and_preserves_existing_files :: proc(t: ^testing.T) {
|
|
root := "/tmp/brolang-test-init-project"
|
|
output := "/tmp/brolang-test-init-project/build/brolang-test-init-project"
|
|
_ = os2.remove_all(root)
|
|
defer _ = os2.remove_all(root)
|
|
|
|
testing.expect(t, os2.make_directory_all(root) == nil)
|
|
testing.expect(t, init_project(root))
|
|
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/source"))
|
|
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/std"))
|
|
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/ffi"))
|
|
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/vendor"))
|
|
|
|
status := compiler_core.run_build(root)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
|
|
custom_build := "custom build"
|
|
custom_main := "custom main"
|
|
testing.expect(t, os.write_entire_file("/tmp/brolang-test-init-project/build.bro", transmute([]byte)custom_build))
|
|
testing.expect(t, os.write_entire_file("/tmp/brolang-test-init-project/source/main.bro", transmute([]byte)custom_main))
|
|
testing.expect(t, init_project(root))
|
|
build_data, build_ok := os.read_entire_file("/tmp/brolang-test-init-project/build.bro")
|
|
defer delete(build_data)
|
|
main_data, main_ok := os.read_entire_file("/tmp/brolang-test-init-project/source/main.bro")
|
|
defer delete(main_data)
|
|
testing.expect(t, build_ok && string(build_data) == custom_build)
|
|
testing.expect(t, main_ok && string(main_data) == custom_main)
|
|
}
|
|
|
|
@(test)
|
|
build_root_search_finds_nearest_parent_build_file :: proc(t: ^testing.T) {
|
|
root := "/tmp/brolang-test-build-search"
|
|
nested := "/tmp/brolang-test-build-search/source/nested"
|
|
_ = os2.remove_all(root)
|
|
defer _ = os2.remove_all(root)
|
|
|
|
testing.expect(t, os2.make_directory_all(nested) == nil)
|
|
build_text := "# root\n"
|
|
testing.expect(t, os.write_entire_file("/tmp/brolang-test-build-search/build.bro", transmute([]byte)build_text))
|
|
found, ok := compiler_core.find_build_root_from(nested)
|
|
defer delete(found)
|
|
testing.expect(t, ok)
|
|
testing.expect(t, strings.has_suffix(found, "/brolang-test-build-search"))
|
|
}
|
|
|
|
@(test)
|
|
build_subcommand_discovered_root_writes_to_root_build_dir :: proc(t: ^testing.T) {
|
|
root := "/tmp/brolang-test-build-nested"
|
|
nested := "/tmp/brolang-test-build-nested/source/nested"
|
|
output := "/tmp/brolang-test-build-nested/build/brolang-test-build-nested"
|
|
_ = os2.remove_all(root)
|
|
defer _ = os2.remove_all(root)
|
|
|
|
testing.expect(t, init_project(root))
|
|
testing.expect(t, os2.make_directory_all(nested) == nil)
|
|
found, ok := compiler_core.find_build_root_from(nested)
|
|
defer delete(found)
|
|
testing.expect(t, ok)
|
|
|
|
status := compiler_core.run_build(found)
|
|
testing.expect_value(t, status, 0)
|
|
testing.expect(t, os.exists(output))
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
build_subcommand_rejects_path_like_output_name :: proc(t: ^testing.T) {
|
|
root := "/tmp/brolang-test-build-invalid-name"
|
|
_ = os2.remove_all(root)
|
|
defer _ = os2.remove_all(root)
|
|
|
|
testing.expect(t, init_project(root))
|
|
text := `b :: import "@std/build"
|
|
|
|
config :: b.BuildConfig{
|
|
name = "bad/name",
|
|
source = "source",
|
|
libraries = &[],
|
|
lib_paths = &[],
|
|
includes = &[],
|
|
defines = &[],
|
|
links = &[],
|
|
}
|
|
`
|
|
testing.expect(t, os.write_entire_file("/tmp/brolang-test-build-invalid-name/build.bro", transmute([]byte)text))
|
|
status := compiler_core.run_build(root)
|
|
testing.expect_value(t, status, 2)
|
|
testing.expect(t, !os.exists("/tmp/brolang-test-build-invalid-name/build/bad/name"))
|
|
}
|
|
|
|
@(test)
|
|
project_root_imports_resolve_under_passed_root :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
|
|
ast_module, loaded := loader.load(
|
|
"examples/build/raylib/source",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
context.allocator,
|
|
context.allocator,
|
|
cimport.Options{},
|
|
target.DEFAULT,
|
|
"examples/build/raylib",
|
|
)
|
|
defer ast.destroy_module(&ast_module)
|
|
testing.expect(t, loaded)
|
|
|
|
found_vendor := false
|
|
for pkg in ast_module.packages {
|
|
found_vendor = found_vendor || strings.has_suffix(pkg.path, "/examples/build/raylib/vendor/raylib")
|
|
}
|
|
testing.expect(t, found_vendor)
|
|
}
|
|
|
|
@(test)
|
|
direct_compile_defaults_project_root_to_input_package_and_allows_override :: proc(t: ^testing.T) {
|
|
root := "/tmp/brolang-test-direct-root"
|
|
app := "/tmp/brolang-test-direct-root/app"
|
|
dep := "/tmp/brolang-test-direct-root/dep"
|
|
output := "/tmp/brolang-test-direct-root-out"
|
|
_ = os2.remove_all(root)
|
|
_ = os.remove(output)
|
|
defer _ = os2.remove_all(root)
|
|
defer _ = os.remove(output)
|
|
|
|
testing.expect(t, os2.make_directory_all(app) == nil)
|
|
testing.expect(t, os2.make_directory_all(dep) == nil)
|
|
main_text := "dep :: import \"@dep\"\nmain func() i32 { return dep.value }\n"
|
|
dep_text := "value i32 :: 7\n"
|
|
testing.expect(t, os.write_entire_file("/tmp/brolang-test-direct-root/app/main.bro", transmute([]byte)main_text))
|
|
testing.expect(t, os.write_entire_file("/tmp/brolang-test-direct-root/dep/dep.bro", transmute([]byte)dep_text))
|
|
|
|
testing.expect_value(t, compiler_core.compile_package(app, output), 1)
|
|
status := compiler_core.compile_package(app, output, nil, target.DEFAULT, cimport.Options{}, root)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 7)
|
|
}
|
|
|
|
@(test)
|
|
c_printf_accepts_a_string_literal :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-printf"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/interop/printf", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
sentinel_pointer_views_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-sentinel-pointer"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/sentinel_pointer", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 303)
|
|
}
|
|
|
|
@(test)
|
|
milestone_24_regressions_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-milestone-24"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/milestone_24", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
milestone_24_rejects_invalid_forms :: proc(t: ^testing.T) {
|
|
Case :: struct {
|
|
directory: string,
|
|
output: string,
|
|
}
|
|
cases := [?]Case{
|
|
{"examples/programs/index_signed_error", "/tmp/brolang-test-index-signed-error"},
|
|
{"examples/programs/index_int_constraint_error", "/tmp/brolang-test-index-int-constraint-error"},
|
|
{"examples/programs/scalar_cast_error", "/tmp/brolang-test-scalar-cast-error"},
|
|
{"examples/programs/array_const_size_error", "/tmp/brolang-test-array-const-size-error"},
|
|
}
|
|
for test_case in cases {
|
|
status := compiler_core.compile_package(test_case.directory, test_case.output)
|
|
testing.expect_value(t, status, 1)
|
|
_ = os.remove(test_case.output)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
function_literals_lower_as_function_pointer_values :: proc(t: ^testing.T) {
|
|
text := `Callbacks :: struct {
|
|
call @func(value i32) i32
|
|
value i32
|
|
}
|
|
run func(callbacks Callbacks) i32 {
|
|
return callbacks.call(callbacks.value)
|
|
}
|
|
main func() i32 {
|
|
callbacks Callbacks = Callbacks {
|
|
call = func(value i32) i32 {
|
|
return value + 1
|
|
},
|
|
value = 41,
|
|
}
|
|
return run(callbacks) - 42
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
generated_functions := 0
|
|
literal_exprs := 0
|
|
for function in ast_module.functions {
|
|
generated_functions += 1 if function.generated else 0
|
|
}
|
|
for expr in ast_module.exprs {
|
|
literal_exprs += 1 if expr.kind == .Function_Literal else 0
|
|
}
|
|
indirect_calls := 0
|
|
for expr in hir_module.exprs {
|
|
if expr.kind == .Call && expr.left != hir.INVALID_EXPR {
|
|
indirect_calls += 1
|
|
}
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, generated_functions, 1)
|
|
testing.expect_value(t, literal_exprs, 1)
|
|
testing.expect(t, len(ir_module.functions) > 0)
|
|
testing.expect(t, indirect_calls > 0)
|
|
}
|
|
|
|
@(test)
|
|
qualified_value_calls_do_not_imply_package_resolution :: proc(t: ^testing.T) {
|
|
text := `Callbacks :: struct {
|
|
call @func() void
|
|
}
|
|
main func() void {
|
|
callbacks Callbacks = Callbacks{call = func() void {}}
|
|
callbacks.call()
|
|
missing.call()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'")
|
|
testing.expect(t, !strings.contains(diagnostics.items[0].message, "package"))
|
|
}
|
|
|
|
@(test)
|
|
function_literals_do_not_capture_locals :: proc(t: ^testing.T) {
|
|
text := `main func() i32 {
|
|
offset i32 = 1
|
|
callback @func(value i32) i32 = func(value i32) i32 {
|
|
return value + offset
|
|
}
|
|
return callback(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "unknown symbol 'offset'")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
field_function_pointer_calls_reject_non_callable_fields :: proc(t: ^testing.T) {
|
|
text := `Box :: struct {
|
|
value i32
|
|
}
|
|
main func() void {
|
|
box Box = Box { value = 1 }
|
|
box.value()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "call target is not a function pointer")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
allocator_contract_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-mem-allocator"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/mem_allocator", output, nil, target.DEFAULT, cimport.Options{}, ".")
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load("examples/programs/mem_allocator", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".")
|
|
defer ast.destroy_module(&ast_module)
|
|
testing.expect(t, loaded)
|
|
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
found_c_allocator := false
|
|
found_anyopaque_context := false
|
|
found_vtable_pointer := false
|
|
found_alloc_callback := false
|
|
found_realloc_callback := false
|
|
found_free_callback := false
|
|
for global in hir_module.globals {
|
|
if symbol.resolve(&symbols, global.name) != "c_allocator" {
|
|
continue
|
|
}
|
|
found_c_allocator = true
|
|
for field in types.fields_for(&hir_module.types, global.type) {
|
|
name := symbol.resolve(&symbols, symbol.Id(field.name))
|
|
if name == "context" {
|
|
optional_item, optional_ok := types.node(&hir_module.types, field.type)
|
|
if optional_ok && optional_item.kind == .Optional {
|
|
pointer_item, pointer_ok := types.node(&hir_module.types, optional_item.child)
|
|
found_anyopaque_context = pointer_ok &&
|
|
pointer_item.kind == .Pointer &&
|
|
pointer_item.mutable &&
|
|
!pointer_item.many &&
|
|
pointer_item.child == types.ANYOPAQUE
|
|
}
|
|
} else if name == "vtable" {
|
|
pointer_item, pointer_ok := types.node(&hir_module.types, field.type)
|
|
found_vtable_pointer = pointer_ok && pointer_item.kind == .Pointer && !pointer_item.mutable && !pointer_item.many
|
|
if found_vtable_pointer {
|
|
for callback in types.fields_for(&hir_module.types, pointer_item.child) {
|
|
callback_name := symbol.resolve(&symbols, symbol.Id(callback.name))
|
|
callback_pointer, _, _, callable := types.function_pointer(callback.type, &hir_module.types)
|
|
found_alloc_callback = found_alloc_callback || callback_name == "alloc" && callable && !callback_pointer.many
|
|
found_realloc_callback = found_realloc_callback || callback_name == "realloc" && callable && !callback_pointer.many
|
|
found_free_callback = found_free_callback || callback_name == "free" && callable && !callback_pointer.many
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, len(ir_module.functions) > 0)
|
|
testing.expect(t, found_c_allocator)
|
|
testing.expect(t, found_anyopaque_context)
|
|
testing.expect(t, found_vtable_pointer)
|
|
testing.expect(t, found_alloc_callback)
|
|
testing.expect(t, found_realloc_callback)
|
|
testing.expect(t, found_free_callback)
|
|
}
|
|
|
|
@(test)
|
|
milestone_25_c_allocator_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-c-allocator"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/mem_allocator", output, nil, target.DEFAULT, cimport.Options{}, ".")
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
milestone_25_c_allocator_emits_libc_alloc_declarations :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load("examples/programs/mem_allocator", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".")
|
|
defer ast.destroy_module(&ast_module)
|
|
testing.expect(t, loaded)
|
|
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "declare ptr @malloc(i64)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare ptr @realloc(ptr, i64)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @posix_memalign(ptr, i64, i64)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare void @free(ptr)"))
|
|
}
|
|
|
|
@(test)
|
|
control_flow_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-control-flow"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/control_flow", output)
|
|
testing.expect_value(t, status, 0)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}},
|
|
context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
// if / else if / else, comparisons, logical and/or/not, bool locals, and
|
|
// block scoping together produce 42.
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
// Short-circuit: `noisy()` is never reached, so its output must be absent,
|
|
// while the taken or-branch must print.
|
|
testing.expect(t, !strings.contains(string(stdout), "rhs-evaluated"))
|
|
testing.expect(t, strings.contains(string(stdout), "or-taken"))
|
|
}
|
|
|
|
@(test)
|
|
break_and_continue_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-break-continue"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/break_continue", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
// `while` break, range-for `continue`, nested innermost-targeting break, a
|
|
// `continue` on the final element of an inclusive `u8` range (no overflow
|
|
// trap), and an exitable `while true` together produce 42.
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
break_and_continue_misuse_is_diagnosed :: proc(t: ^testing.T) {
|
|
// `break`/`continue` outside any loop are rejected, and a non-void function
|
|
// that exits a `while true` via `break` without returning is flagged as
|
|
// missing a return (the `all_paths_return` refinement).
|
|
text := `main func() i32 {
|
|
bad_break()
|
|
bad_continue()
|
|
return missing_return()
|
|
}
|
|
bad_break func() void {
|
|
break
|
|
}
|
|
bad_continue func() void {
|
|
continue
|
|
}
|
|
missing_return func() i32 {
|
|
while true {
|
|
break
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
break_outside := false
|
|
continue_outside := false
|
|
missing := false
|
|
for diagnostic in diagnostics.items {
|
|
break_outside = break_outside || strings.contains(diagnostic.message, "'break' outside of a loop")
|
|
continue_outside = continue_outside || strings.contains(diagnostic.message, "'continue' outside of a loop")
|
|
missing = missing || strings.contains(diagnostic.message, "'missing_return' does not return a value")
|
|
}
|
|
testing.expect(t, break_outside)
|
|
testing.expect(t, continue_outside)
|
|
testing.expect(t, missing)
|
|
}
|
|
|
|
@(test)
|
|
defer_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-defer"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/defer", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
// LIFO, runs on fall-through / break / continue / return (with the return value
|
|
// captured before defers run), scoped bare blocks, and `defer { ... }` blocks
|
|
// together produce 42.
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
defer_misuse_is_diagnosed :: proc(t: ^testing.T) {
|
|
// Deferring control flow that would escape the defer is rejected: `defer return`,
|
|
// `defer break`, and a `return` inside a `defer { ... }` block.
|
|
text := `main func() i32 {
|
|
bad_defer_return()
|
|
bad_defer_break()
|
|
bad_return_in_defer()
|
|
bad_errdefer_nonfallible()
|
|
_ = bad_try_in_defer() catch 0
|
|
_ = bad_try_in_errdefer() catch 0
|
|
return 0
|
|
}
|
|
Failure :: enum { bad }
|
|
fail func() i32 ! Failure { return .bad }
|
|
bad_defer_return func() void {
|
|
defer return
|
|
}
|
|
bad_defer_break func() void {
|
|
for 0..3 |i| {
|
|
defer break
|
|
_ = i
|
|
}
|
|
}
|
|
bad_return_in_defer func() void {
|
|
defer {
|
|
return
|
|
}
|
|
}
|
|
bad_errdefer_nonfallible func() void {
|
|
errdefer {}
|
|
}
|
|
bad_try_in_defer func() i32 ! Failure {
|
|
defer _ = try fail()
|
|
return 1
|
|
}
|
|
bad_try_in_errdefer func() i32 ! Failure {
|
|
errdefer _ = try fail()
|
|
return 1
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
defer_return := false
|
|
defer_break := false
|
|
return_in_defer := false
|
|
errdefer_nonfallible := false
|
|
try_in_defer := 0
|
|
for diagnostic in diagnostics.items {
|
|
defer_return = defer_return || strings.contains(diagnostic.message, "cannot defer a 'return' statement")
|
|
defer_break = defer_break || strings.contains(diagnostic.message, "cannot defer a 'break' statement")
|
|
return_in_defer = return_in_defer || strings.contains(diagnostic.message, "cannot 'return' inside a 'defer'")
|
|
errdefer_nonfallible = errdefer_nonfallible || strings.contains(diagnostic.message, "'errdefer' requires an enclosing fallible function")
|
|
try_in_defer += 1 if strings.contains(diagnostic.message, "cannot 'try' inside a 'defer'") else 0
|
|
}
|
|
testing.expect(t, defer_return)
|
|
testing.expect(t, defer_break)
|
|
testing.expect(t, return_in_defer)
|
|
testing.expect(t, errdefer_nonfallible)
|
|
testing.expect_value(t, try_in_defer, 2)
|
|
}
|
|
|
|
@(test)
|
|
errdefer_capture_syntax_is_diagnosed :: proc(t: ^testing.T) {
|
|
text := `Failure :: enum { bad }
|
|
bad func() i32 ! Failure {
|
|
errdefer || {}
|
|
errdefer |first, second| {}
|
|
return .bad
|
|
}
|
|
main func() i32 { return bad() catch 0 }
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
missing := false
|
|
multiple := false
|
|
for diagnostic in diagnostics.items {
|
|
missing = missing || strings.contains(diagnostic.message, "expected an errdefer capture name")
|
|
multiple = multiple || strings.contains(diagnostic.message, "'errdefer' accepts exactly one capture")
|
|
}
|
|
testing.expect(t, missing)
|
|
testing.expect(t, multiple)
|
|
}
|
|
|
|
@(test)
|
|
yield_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-yield"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/yield", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
// Value blocks, value if-statements (incl. `return` branches and unwrap-`if`),
|
|
// `orelse`, value loops, labeled value blocks (`blk: { … yield :blk v }`, incl.
|
|
// defer-capture, `{T,none}` → optional, and `none` before a concrete yield that uses a
|
|
// block local), outer-loop control (`yield :outer v` / `break :outer`), and labeled
|
|
// block statements exited via `break :blk` together produce 42.
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
value_loop_label_does_not_shadow_own_yield_target :: proc(t: ^testing.T) {
|
|
text := `main func() i32 {
|
|
idx :: for 0..10 |i| hit: {
|
|
if (i == 3) yield :hit i
|
|
yield none
|
|
}
|
|
if idx |found| {
|
|
if (found == 3) return 0
|
|
return 1
|
|
}
|
|
return 2
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
native_union_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-unions"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/unions", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
// A native untagged union (`Val :: union { n i32, f f64 }`) constructed via a keyed
|
|
// literal, stored into a local, and read back through field access reinterprets the
|
|
// carrier and yields 42 (declaration → construct → store → load → field read).
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
tagged_union_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-tagged-union"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/tagged_union", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
// Both tagged forms — `union(Animal)` (existing enum tag) and `union(enum)` (synthesized
|
|
// tag) — constructed via keyed literals, stored as `{tag, payload}`, with the active
|
|
// payload read back at its post-tag offset: 37 + 5 = 42. Non-zero payloads make a wrong
|
|
// payload offset (e.g. overlapping the tag) fail the exit code.
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
tagged_union_stores_the_discriminant :: proc(t: ^testing.T) {
|
|
// The runtime test observes only the payload; this one checks the *tag* is written.
|
|
// Native sum tags are global u16 IDs. `Animal` contributes dog/cat/bird (1..3),
|
|
// then `Data` contributes dog:i32/bird:i32 (4..5), so `Data{ bird = 99 }`
|
|
// writes discriminant `store i16 5` beside the payload.
|
|
text := `Animal :: enum {
|
|
dog
|
|
cat
|
|
bird
|
|
}
|
|
Data :: union(Animal) {
|
|
dog i32
|
|
bird i32
|
|
}
|
|
main func() i32 {
|
|
x Data = Data{ bird = 99 }
|
|
return x.bird
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "{ i16, [2 x i8], i32 }"))
|
|
testing.expect(t, strings.contains(llvm_text, "store i16 5,"))
|
|
testing.expect(t, strings.contains(llvm_text, "store i32 99,"))
|
|
}
|
|
|
|
@(test)
|
|
tagged_union_validation_is_diagnosed :: proc(t: ^testing.T) {
|
|
// A `union(T)` tag must be an enum, and every variant of a `union(Enum)` must name a
|
|
// member of that enum.
|
|
text := `Color :: struct {
|
|
r u8
|
|
}
|
|
Animal :: enum {
|
|
dog
|
|
cat
|
|
}
|
|
BadTag :: union(Color) {
|
|
dog i32
|
|
}
|
|
BadVariant :: union(Animal) {
|
|
snake i32
|
|
}
|
|
main func() i32 {
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_tag := false
|
|
found_variant := false
|
|
for diagnostic in diagnostics.items {
|
|
found_tag = found_tag || strings.contains(diagnostic.message, "tagged union's tag must be an enum")
|
|
found_variant = found_variant || strings.contains(diagnostic.message, "'snake' is not a member of the tag enum")
|
|
}
|
|
testing.expect(t, found_tag)
|
|
testing.expect(t, found_variant)
|
|
}
|
|
|
|
@(test)
|
|
sum_composition_merges_widens_and_rejects_conflicts :: proc(t: ^testing.T) {
|
|
text := `A :: enum {
|
|
same
|
|
left
|
|
}
|
|
B :: enum {
|
|
same
|
|
right
|
|
}
|
|
Both :: alias A | B
|
|
UA :: union(enum) {
|
|
item i32
|
|
}
|
|
UB :: union(enum) {
|
|
empty void
|
|
}
|
|
UBoth :: alias UA | UB
|
|
pick func(value Both) i32 {
|
|
match value {
|
|
.same: return 1
|
|
.left: return 2
|
|
.right: return 3
|
|
}
|
|
}
|
|
payload func(value UBoth) i32 {
|
|
match value {
|
|
.item |n|: return n
|
|
.empty: return 5
|
|
}
|
|
}
|
|
main func() i32 {
|
|
a A = .left
|
|
b B = .right
|
|
u UA = UA{ item = 4 }
|
|
v UB = .empty
|
|
return pick(.same) + pick(a) + pick(b) + payload(u) + payload(v)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64"))
|
|
|
|
conflict := `A :: union(enum) {
|
|
dup i32
|
|
}
|
|
B :: union(enum) {
|
|
dup i64
|
|
}
|
|
Bad :: alias A | B
|
|
main func() void {}
|
|
`
|
|
conflict_source := source.Source{path="conflict.bro", text=conflict}
|
|
conflict_diagnostics := source.init_diagnostics(&conflict_source)
|
|
defer source.destroy_diagnostics(&conflict_diagnostics)
|
|
conflict_symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&conflict_symbols)
|
|
conflict_stream := lexer.lex(&conflict_source, &conflict_diagnostics, &conflict_symbols)
|
|
defer delete(conflict_stream.items)
|
|
conflict_module := parser.parse(&conflict_stream, &conflict_source, &conflict_diagnostics)
|
|
defer ast.destroy_module(&conflict_module)
|
|
found_conflict := false
|
|
for diagnostic in conflict_diagnostics.items {
|
|
found_conflict = found_conflict || strings.contains(diagnostic.message, "same variant name")
|
|
}
|
|
testing.expect(t, found_conflict)
|
|
|
|
backed := `A :: enum(u8) {
|
|
a = 1
|
|
}
|
|
B :: enum {
|
|
b
|
|
}
|
|
Bad :: alias A | B
|
|
main func() void {}
|
|
`
|
|
backed_source := source.Source{path="backed.bro", text=backed}
|
|
backed_diagnostics := source.init_diagnostics(&backed_source)
|
|
defer source.destroy_diagnostics(&backed_diagnostics)
|
|
backed_symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&backed_symbols)
|
|
backed_stream := lexer.lex(&backed_source, &backed_diagnostics, &backed_symbols)
|
|
defer delete(backed_stream.items)
|
|
backed_module := parser.parse(&backed_stream, &backed_source, &backed_diagnostics)
|
|
defer ast.destroy_module(&backed_module)
|
|
found_backed := false
|
|
for diagnostic in backed_diagnostics.items {
|
|
found_backed = found_backed || strings.contains(diagnostic.message, "native unbacked")
|
|
}
|
|
testing.expect(t, found_backed)
|
|
}
|
|
|
|
@(test)
|
|
yield_inference_visits_yielded_calls :: proc(t: ^testing.T) {
|
|
text := `identity func(value int) int {
|
|
return value
|
|
}
|
|
main func() i32 {
|
|
value :: {
|
|
yield identity(41)
|
|
}
|
|
return value - 41
|
|
}
|
|
`
|
|
source_file := source.Source{path="yield_call.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, len(hir_module.functions) > 1)
|
|
}
|
|
|
|
@(test)
|
|
fallible_ergonomics_rejects_bad_try_and_catch_blocks :: proc(t: ^testing.T) {
|
|
text := `A :: enum {
|
|
a
|
|
}
|
|
B :: enum {
|
|
b
|
|
}
|
|
fa func() i32 ! A {
|
|
return .a
|
|
}
|
|
bad_error func() i32 ! B {
|
|
x :: try fa()
|
|
return x
|
|
}
|
|
bad_catch func() i32 {
|
|
return fa() catch |e| {
|
|
_ = e
|
|
}
|
|
}
|
|
bad_nested_match func() i32 {
|
|
return fa() catch |e| {
|
|
match e {
|
|
.a: yield 3
|
|
}
|
|
}
|
|
}
|
|
main func() i32 {
|
|
_ = bad_error() catch 0
|
|
return bad_catch() + bad_nested_match()
|
|
}
|
|
`
|
|
source_file := source.Source{path="fallible_ergonomics.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_error := false
|
|
found_yield := false
|
|
found_misplaced_yield := false
|
|
for diagnostic in diagnostics.items {
|
|
found_error = found_error || strings.contains(diagnostic.message, "'try' error channel cannot be widened")
|
|
found_yield = found_yield || strings.contains(diagnostic.message, "a value block must end with an explicit 'yield'")
|
|
found_misplaced_yield = found_misplaced_yield || strings.contains(diagnostic.message, "'yield' is only valid as the final statement")
|
|
}
|
|
testing.expect(t, found_error)
|
|
testing.expect(t, found_yield)
|
|
testing.expect(t, found_misplaced_yield)
|
|
|
|
success_text := `A :: enum {
|
|
a
|
|
}
|
|
B :: enum {
|
|
b
|
|
}
|
|
Both :: alias A | B
|
|
fs func() i64 ! A {
|
|
return 1
|
|
}
|
|
bad_success func() i32 ! Both {
|
|
x :: try fs()
|
|
return x
|
|
}
|
|
main func() i32 {
|
|
return bad_success() catch 0
|
|
}
|
|
`
|
|
directory := "/tmp/brolang-test-try-success-mismatch"
|
|
main_path := "/tmp/brolang-test-try-success-mismatch/main.bro"
|
|
output := "/tmp/brolang-test-try-success-mismatch-output"
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)success_text))
|
|
testing.expect_value(t, compiler_core.compile_package(directory, output), 1)
|
|
}
|
|
|
|
@(test)
|
|
terminating_catch_block_compiles_and_runs :: proc(t: ^testing.T) {
|
|
text := `Failure :: enum {
|
|
bad
|
|
}
|
|
may_fail func(fail bool) i32 ! Failure {
|
|
if (fail) return .bad
|
|
return 1
|
|
}
|
|
recover func(fail bool) i32 {
|
|
value :: may_fail(fail) catch |_| {
|
|
return 40
|
|
}
|
|
return value + 1
|
|
}
|
|
main func() i32 {
|
|
return recover(false) + recover(true) - 42
|
|
}
|
|
`
|
|
directory := "/tmp/brolang-test-terminating-catch"
|
|
main_path := "/tmp/brolang-test-terminating-catch/main.bro"
|
|
output := "/tmp/brolang-test-terminating-catch-output"
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
fallible_void_fallthrough_compiles_at_runtime_and_comptime :: proc(t: ^testing.T) {
|
|
text := `Failure :: enum {
|
|
bad
|
|
}
|
|
succeed func() void ! Failure {}
|
|
fail func() void ! Failure { return .bad }
|
|
maybe func(should_fail bool) void ! Failure {
|
|
if should_fail { return .bad }
|
|
}
|
|
recover func(exit bool) i32 {
|
|
fail() catch |_| {
|
|
if exit { return 7 }
|
|
}
|
|
return 0
|
|
}
|
|
comptime_scenario func() i32 {
|
|
succeed() catch |_| {}
|
|
fail() catch |_| {}
|
|
return 0
|
|
}
|
|
main func() i32 {
|
|
comptime_result i32 :: $comptime_scenario()
|
|
handled bool = false
|
|
fail() catch |_| { handled = true }
|
|
if !handled { return 3 }
|
|
succeed() catch |_| { return 1 }
|
|
maybe(false) catch |_| { return 2 }
|
|
return comptime_result + recover(false) + recover(true) - 7
|
|
}
|
|
`
|
|
directory := "/tmp/brolang-test-fallible-void-fallthrough"
|
|
main_path := "/tmp/brolang-test-fallible-void-fallthrough/main.bro"
|
|
output := "/tmp/brolang-test-fallible-void-fallthrough-output"
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
conversion_diagnostics_render_source_types :: proc(t: ^testing.T) {
|
|
text := `Allocator :: struct {
|
|
marker i32
|
|
}
|
|
take func(allocator Allocator, memory []mut u8) void {}
|
|
main func() void {
|
|
allocator Allocator = Allocator { marker = 0 }
|
|
data [1]mut u8 = [0]
|
|
take(data[..], allocator)
|
|
}
|
|
`
|
|
source_file := source.Source{path="type_labels.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
slice_to_allocator := false
|
|
allocator_to_slice := false
|
|
internal_type_id := false
|
|
for diagnostic in diagnostics.items {
|
|
slice_to_allocator = slice_to_allocator ||
|
|
strings.contains(diagnostic.message, "cannot implicitly convert []mut u8 to Allocator")
|
|
allocator_to_slice = allocator_to_slice ||
|
|
strings.contains(diagnostic.message, "cannot implicitly convert Allocator to []mut u8")
|
|
internal_type_id = internal_type_id || strings.contains(diagnostic.message, "<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
|
|
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)
|
|
yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
|
|
// Value if/loop/block misuse: an `if` value without an `else`; a branch that neither
|
|
// yields nor exits on every path (20.6); a value loop whose body lacks a trailing
|
|
// fall-through `yield`; a `yield :label` with no matching value loop; a labeled value
|
|
// block that does not yield on every path; a `break :label` naming no loop; and a
|
|
// `continue :label` targeting a block (not a loop).
|
|
text := `main func() i32 {
|
|
noelse :: if (true) {
|
|
yield 1
|
|
}
|
|
badbranch :: if (true) {
|
|
k :: 5
|
|
} else {
|
|
yield 2
|
|
}
|
|
noloopyield :: for 0..10 |i| blk: {
|
|
if (i == 0) yield :blk i
|
|
}
|
|
for 0..10 |j| stray: {
|
|
yield :stray j
|
|
}
|
|
noblockyield :: blk: {
|
|
if (true) yield :blk 1
|
|
k2 :: 5
|
|
}
|
|
for 0..10 |m| {
|
|
break :nope
|
|
}
|
|
scope: {
|
|
continue :scope
|
|
}
|
|
return noelse + badbranch + noloopyield + noblockyield
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
no_else := false
|
|
branch_no_yield := false
|
|
loop_no_yield := false
|
|
stray_label := false
|
|
block_no_yield := false
|
|
bad_break_label := false
|
|
continue_block := false
|
|
for diagnostic in diagnostics.items {
|
|
no_else = no_else || strings.contains(diagnostic.message, "an 'if' used as a value must have an 'else'")
|
|
branch_no_yield = branch_no_yield || strings.contains(diagnostic.message, "a value branch must end with 'yield' or exit on every path")
|
|
loop_no_yield = loop_no_yield || strings.contains(diagnostic.message, "a value loop's body must end with a 'yield'")
|
|
stray_label = stray_label || strings.contains(diagnostic.message, "no enclosing value loop or block is labeled 'stray'")
|
|
block_no_yield = block_no_yield || strings.contains(diagnostic.message, "a labeled value block must 'yield' on every path")
|
|
bad_break_label = bad_break_label || strings.contains(diagnostic.message, "no enclosing loop is labeled 'nope'")
|
|
// `continue :scope` targets a labeled block, which is not a loop.
|
|
continue_block = continue_block || strings.contains(diagnostic.message, "no enclosing loop is labeled 'scope'")
|
|
}
|
|
testing.expect(t, no_else)
|
|
testing.expect(t, branch_no_yield)
|
|
testing.expect(t, loop_no_yield)
|
|
testing.expect(t, stray_label)
|
|
testing.expect(t, block_no_yield)
|
|
testing.expect(t, bad_break_label)
|
|
testing.expect(t, continue_block)
|
|
}
|
|
|
|
@(test)
|
|
foreign_function_links_from_c_source :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-foreign-source"
|
|
defer _ = os.remove(output)
|
|
arguments := []linker.Argument{{kind=.Input, value="examples/interop/manual/native.c"}}
|
|
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
restricted_c_header_imports_compile_and_link :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-header-import"
|
|
defer _ = os.remove(output)
|
|
arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}}
|
|
c_options := cimport.Options{
|
|
include_paths=[]string{"examples/interop/header/include"},
|
|
defines=[]string{"BROLANG_FEATURE"},
|
|
}
|
|
status := compiler_core.compile_package("examples/interop/header/app", output, arguments, target.DEFAULT, c_options)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
by_value_c_records_and_unions_compile_and_link :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-records"
|
|
defer _ = os.remove(output)
|
|
arguments := []linker.Argument{{kind=.Input, value="examples/interop/records/native.c"}}
|
|
c_options := cimport.Options{include_paths=[]string{"examples/interop/records/include"}}
|
|
status := compiler_core.compile_package("examples/interop/records/app", output, arguments, target.DEFAULT, c_options)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 1)
|
|
}
|
|
|
|
@(test)
|
|
self_referential_c_records_import_and_compile :: proc(t: ^testing.T) {
|
|
// `struct Node { struct Node *next; int value; }`: the importer must not recurse
|
|
// forever populating the self-referential record, and the checker must accept the
|
|
// self-referential `?*mut Node` field as C-layout-compatible.
|
|
output := "/tmp/brolang-test-recursive"
|
|
defer _ = os.remove(output)
|
|
c_options := cimport.Options{include_paths=[]string{"examples/interop/recursive/include"}}
|
|
status := compiler_core.compile_package("examples/interop/recursive/app", output, nil, target.DEFAULT, c_options)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
unsupported_c_header_members_diagnose_only_when_referenced :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-header-unsupported"
|
|
defer _ = os.remove(output)
|
|
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
|
|
status := compiler_core.compile_package("examples/interop/header_unsupported", output, nil, target.DEFAULT, c_options)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
compatible_c_header_redeclarations_share_one_llvm_declaration :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-header-duplicate"
|
|
defer _ = os.remove(output)
|
|
arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}}
|
|
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
|
|
status := compiler_core.compile_package("examples/interop/header_duplicate", output, arguments, target.DEFAULT, c_options)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
interop_foundation_matches_zig_compiled_apple_silicon_c_fixture :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-interop-foundation"
|
|
defer _ = os.remove(output)
|
|
arguments := []linker.Argument{{kind=.Input, value="examples/interop/foundation/native.c"}}
|
|
status := compiler_core.compile_package("examples/interop/foundation", output, arguments)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 1)
|
|
}
|
|
|
|
@(test)
|
|
foreign_function_links_from_object :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-foreign-object"
|
|
object := "/tmp/brolang-test-foreign-object.o"
|
|
defer _ = os.remove(output)
|
|
defer _ = os.remove(object)
|
|
testing.expect(t, prepare_native_object(object))
|
|
arguments := []linker.Argument{{kind=.Input, value=object}}
|
|
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
foreign_function_links_from_direct_library :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-foreign-direct-library"
|
|
object := "/tmp/brolang-test-foreign-direct-library.o"
|
|
archive := "/tmp/brolang-test-foreign-direct-library.a"
|
|
defer _ = os.remove(output)
|
|
defer _ = os.remove(object)
|
|
defer _ = os.remove(archive)
|
|
testing.expect(t, prepare_native_object(object))
|
|
testing.expect(t, prepare_native_archive(object, archive))
|
|
arguments := []linker.Argument{{kind=.Input, value=archive}}
|
|
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
foreign_function_links_from_named_library :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-foreign-named-library"
|
|
object := "/tmp/brolang-test-foreign-named-library.o"
|
|
archive := "/tmp/libbrolang-test-foreign-named.a"
|
|
defer _ = os.remove(output)
|
|
defer _ = os.remove(object)
|
|
defer _ = os.remove(archive)
|
|
testing.expect(t, prepare_native_object(object))
|
|
testing.expect(t, prepare_native_archive(object, archive))
|
|
arguments := []linker.Argument{
|
|
{kind=.Library_Path, value="/tmp"},
|
|
{kind=.Library, value="brolang-test-foreign-named"},
|
|
}
|
|
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
one_line_main_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-one-line-main"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/one_line", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 7)
|
|
}
|
|
|
|
@(test)
|
|
folded_constant_addition_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-constant-fold"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/constant_fold", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
main_int_is_constrained_to_i32 :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-main-int"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/main_int", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 3)
|
|
}
|
|
|
|
@(test)
|
|
main_i32_returns_directly :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-main-i32"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/main_i32", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 4)
|
|
}
|
|
|
|
@(test)
|
|
checked_addition_traps_on_overflow :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-overflow"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/overflow", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
checked_runtime_negation_traps_for_every_signed_width :: proc(t: ^testing.T) {
|
|
Case :: struct {
|
|
type_name: string,
|
|
magnitude: string,
|
|
}
|
|
cases := [?]Case{
|
|
{type_name="i8", magnitude="128"},
|
|
{type_name="i16", magnitude="32768"},
|
|
{type_name="i32", magnitude="2147483648"},
|
|
{type_name="i64", magnitude="9223372036854775808"},
|
|
}
|
|
for test_case in cases {
|
|
directory := fmt.aprintf("/tmp/brolang-test-negate-overflow-%s", test_case.type_name)
|
|
main_path := fmt.aprintf("%s/main.bro", directory)
|
|
output := fmt.aprintf("/tmp/brolang-test-negate-overflow-output-%s", test_case.type_name)
|
|
builder := strings.builder_make()
|
|
fmt.sbprintf(
|
|
&builder,
|
|
"negate func(value %s) %s {{ return -value }}\nmain func() void {{ _ = negate(-%s) }}\n",
|
|
test_case.type_name,
|
|
test_case.type_name,
|
|
test_case.magnitude,
|
|
)
|
|
text := strings.clone(strings.to_string(builder))
|
|
strings.builder_destroy(&builder)
|
|
_ = os2.remove_all(directory)
|
|
_ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
status := compiler_core.compile_package(directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
_ = os.remove(output)
|
|
_ = os2.remove_all(directory)
|
|
delete(text)
|
|
delete(output)
|
|
delete(main_path)
|
|
delete(directory)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
constant_that_does_not_fit_context_produces_trap_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-constant-context-error"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/constant_context_error", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
constant_beyond_i64_produces_trap_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-constant-i64-overflow"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/constant_i64_overflow", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
same_line_statements_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := "main func() void { _ = 1 _ = 2 }\n"
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, len(diagnostics.items) > 0)
|
|
}
|
|
|
|
@(test)
|
|
missing_main_produces_trap_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-missing-main"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/missing_main", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
backend_failure_preserves_existing_output :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-preserved-output"
|
|
defer _ = os.remove(output)
|
|
previous := "previous artifact"
|
|
testing.expect(t, os.write_entire_file(output, transmute([]byte)previous))
|
|
testing.expect(t, !backend.compile("/definitely/not/llvm.ll", output))
|
|
data, ok := os.read_entire_file(output)
|
|
defer delete(data)
|
|
testing.expect(t, ok)
|
|
testing.expect_value(t, string(data), previous)
|
|
}
|
|
|
|
@(test)
|
|
backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T) {
|
|
arguments := []linker.Argument{
|
|
{kind=.Input, value="native.c"},
|
|
{kind=.Library_Path, value="vendor/lib"},
|
|
{kind=.Library, value="thing"},
|
|
{kind=.Input, value="helper.o"},
|
|
}
|
|
c_options := cimport.Options{
|
|
include_paths=[]string{"vendor/include"},
|
|
defines=[]string{"FEATURE=1"},
|
|
}
|
|
command := backend.build_command("module.ll", "program", arguments, target.DEFAULT, c_options)
|
|
defer backend.destroy_command(command)
|
|
|
|
expected_prefix := []string{
|
|
"/usr/bin/env",
|
|
"zig",
|
|
"cc",
|
|
"-target",
|
|
"aarch64-macos",
|
|
"-Wno-override-module",
|
|
"-Wno-unused-command-line-argument",
|
|
}
|
|
testing.expect(t, len(command) >= len(expected_prefix))
|
|
for value, index in expected_prefix {
|
|
testing.expect_value(t, command[index], value)
|
|
}
|
|
|
|
tail_index := len(expected_prefix)
|
|
if sdk, ok := backend.macos_sdk_root(); ok {
|
|
defer delete(sdk)
|
|
frameworks := fmt.tprintf("-F%s/System/Library/Frameworks", sdk)
|
|
lib_dir := fmt.tprintf("-L%s/usr/lib", sdk)
|
|
testing.expect(t, len(command) >= tail_index + 2)
|
|
testing.expect_value(t, command[tail_index], frameworks)
|
|
testing.expect_value(t, command[tail_index + 1], lib_dir)
|
|
tail_index += 2
|
|
}
|
|
|
|
expected_tail := []string{
|
|
"module.ll",
|
|
"-Ivendor/include",
|
|
"-DFEATURE=1",
|
|
"native.c",
|
|
"-Lvendor/lib",
|
|
"-lthing",
|
|
"helper.o",
|
|
"-o",
|
|
"program",
|
|
}
|
|
testing.expect_value(t, len(command), tail_index + len(expected_tail))
|
|
for value, index in expected_tail {
|
|
testing.expect_value(t, command[tail_index + index], value)
|
|
}
|
|
}
|
|
|
|
Fake_Cimport_State :: struct {
|
|
calls: int,
|
|
available: bool,
|
|
infrastructure: bool,
|
|
saw_options: bool,
|
|
}
|
|
|
|
Conflict_Cimport_State :: struct {
|
|
calls: int,
|
|
}
|
|
|
|
Symbol_Conflict_Cimport_State :: struct {
|
|
calls: int,
|
|
}
|
|
|
|
fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
state := (^Fake_Cimport_State)(user_data)
|
|
state.calls += 1
|
|
state.saw_options =
|
|
len(request.include_paths) == 2 &&
|
|
request.include_paths[0] == "first/include" &&
|
|
request.include_paths[1] == "second/include" &&
|
|
len(request.defines) == 2 &&
|
|
request.defines[0] == "FIRST=1" &&
|
|
request.defines[1] == "SECOND" &&
|
|
request.target == target.DEFAULT
|
|
result := cimport.init_result(allocator)
|
|
if !state.available {
|
|
result.infrastructure = state.infrastructure
|
|
result.error_message = fmt.aprintf("fake importer unavailable", allocator=allocator)
|
|
return result
|
|
}
|
|
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
|
|
append(&result.functions, cimport.Function{
|
|
name=fmt.aprintf("fake_value", allocator=allocator),
|
|
result=cimport.Type_Id(0),
|
|
variadic=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("fake_global", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
append(&result.macros, cimport.Macro_Constant{
|
|
name=fmt.aprintf("FAKE_MAGIC", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
value={kind=.Integer, type=cimport.Type_Id(0), integer=7},
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
state := (^Conflict_Cimport_State)(user_data)
|
|
state.calls += 1
|
|
result := cimport.init_result(allocator)
|
|
kind := cimport.Type_Kind.C_Int
|
|
if strings.has_suffix(request.path, "second.h") {
|
|
kind = .C_Long
|
|
}
|
|
append(&result.types, cimport.Type{kind=kind, child=cimport.INVALID_TYPE})
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("conflict_global", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
symbol_conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
state := (^Symbol_Conflict_Cimport_State)(user_data)
|
|
state.calls += 1
|
|
result := cimport.init_result(allocator)
|
|
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
|
|
if strings.has_suffix(request.path, "variable.h") {
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("conflict_symbol", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
} else {
|
|
append(&result.functions, cimport.Function{
|
|
name=fmt.aprintf("conflict_symbol", allocator=allocator),
|
|
result=cimport.Type_Id(0),
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
}
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
main_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
result := cimport.init_result(allocator)
|
|
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("main", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
write_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
result := cimport.init_result(allocator)
|
|
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("write", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
count_substring_occurrences :: proc(text, needle: string) -> int {
|
|
if len(needle) == 0 {
|
|
return 0
|
|
}
|
|
count := 0
|
|
for index := 0; index + len(needle) <= len(text); index += 1 {
|
|
if text[index:index + len(needle)] == needle {
|
|
count += 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
find_substring_offset :: proc(text, needle: string) -> int {
|
|
if len(needle) == 0 {
|
|
return 0
|
|
}
|
|
for index := 0; index + len(needle) <= len(text); index += 1 {
|
|
if text[index:index + len(needle)] == needle {
|
|
return index
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
find_cimport_variable :: proc(result: ^cimport.Result, name: string) -> (^cimport.Variable, bool) {
|
|
for &variable in result.variables {
|
|
if variable.name == name {
|
|
return &variable, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
count_cimport_variables :: proc(result: ^cimport.Result, name: string) -> int {
|
|
count := 0
|
|
for variable in result.variables {
|
|
if variable.name == name {
|
|
count += 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
find_cimport_macro :: proc(result: ^cimport.Result, name: string) -> (^cimport.Macro_Constant, bool) {
|
|
for ¯o in result.macros {
|
|
if macro.name == name {
|
|
return ¯o, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
cimport_has_named_result :: proc(result: ^cimport.Result, name: string) -> bool {
|
|
if _, ok := find_cimport_macro(result, name); ok {
|
|
return true
|
|
}
|
|
for item in result.unsupported {
|
|
if item.name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
@(test)
|
|
cimport_backend_is_replaceable :: proc(t: ^testing.T) {
|
|
state := Fake_Cimport_State{available=true}
|
|
options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}}
|
|
result := cimport.import_header(options, "fake.h")
|
|
defer cimport.destroy_result(&result)
|
|
|
|
testing.expect(t, result.available)
|
|
testing.expect_value(t, state.calls, 1)
|
|
testing.expect_value(t, len(result.functions), 1)
|
|
testing.expect_value(t, len(result.variables), 1)
|
|
testing.expect_value(t, len(result.macros), 1)
|
|
testing.expect_value(t, result.functions[0].name, "fake_value")
|
|
testing.expect_value(t, result.variables[0].name, "fake_global")
|
|
testing.expect_value(t, result.macros[0].name, "FAKE_MAGIC")
|
|
testing.expect(t, result.functions[0].variadic)
|
|
}
|
|
|
|
@(test)
|
|
libclang_import_preserves_external_object_and_final_macro_semantics :: proc(t: ^testing.T) {
|
|
options := cimport.Options{
|
|
include_paths=[]string{"examples/interop/header/include"},
|
|
defines=[]string{"BROLANG_FEATURE"},
|
|
}
|
|
result := cimport.import_header(
|
|
options,
|
|
"examples/interop/header/include/native.h",
|
|
target.DEFAULT,
|
|
)
|
|
defer cimport.destroy_result(&result)
|
|
|
|
testing.expect(t, result.available)
|
|
testing.expect_value(t, result.error_message, "")
|
|
|
|
enum_alias_type := cimport.INVALID_TYPE
|
|
for alias in result.aliases {
|
|
if alias.name == "Imported_Enum" {
|
|
enum_alias_type = alias.type
|
|
break
|
|
}
|
|
}
|
|
testing.expect(t, enum_alias_type != cimport.INVALID_TYPE)
|
|
if enum_alias_type != cimport.INVALID_TYPE {
|
|
testing.expect_value(t, result.types[enum_alias_type].kind, cimport.Type_Kind.C_Int)
|
|
}
|
|
enum_negative, found_enum_negative := find_cimport_macro(&result, "IMPORTED_ENUM_NEGATIVE")
|
|
enum_same, found_enum_same := find_cimport_macro(&result, "IMPORTED_ENUM_SAME")
|
|
enum_value, found_enum_value := find_cimport_macro(&result, "IMPORTED_ENUM_VALUE")
|
|
enum_back, found_enum_back := find_cimport_macro(&result, "IMPORTED_ENUM_BACK")
|
|
enum_anon, found_enum_anon := find_cimport_macro(&result, "IMPORTED_ANON_ENUM")
|
|
testing.expect(t, found_enum_negative && found_enum_same && found_enum_value && found_enum_back && found_enum_anon)
|
|
if found_enum_negative {
|
|
testing.expect(t, enum_negative.value.negative)
|
|
testing.expect_value(t, enum_negative.value.integer, u64(2))
|
|
}
|
|
if found_enum_same {
|
|
testing.expect(t, enum_same.value.negative)
|
|
testing.expect_value(t, enum_same.value.integer, u64(2))
|
|
}
|
|
if found_enum_value {
|
|
testing.expect(t, !enum_value.value.negative)
|
|
testing.expect_value(t, enum_value.value.integer, u64(7))
|
|
}
|
|
if found_enum_back {
|
|
testing.expect_value(t, enum_back.value.integer, u64(3))
|
|
}
|
|
if found_enum_anon {
|
|
testing.expect_value(t, enum_anon.value.integer, u64(9))
|
|
}
|
|
|
|
tls, found_tls := find_cimport_variable(&result, "imported_tls_global")
|
|
testing.expect(t, found_tls)
|
|
if found_tls {
|
|
testing.expect(t, strings.contains(tls.reason, "thread-local C variables are not supported"))
|
|
}
|
|
|
|
const_array, found_const_array := find_cimport_variable(&result, "imported_const_array")
|
|
testing.expect(t, found_const_array)
|
|
if found_const_array {
|
|
testing.expect(t, !const_array.mutable)
|
|
testing.expect(t, const_array.type != cimport.INVALID_TYPE)
|
|
if const_array.type != cimport.INVALID_TYPE {
|
|
array_type := result.types[const_array.type]
|
|
testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array)
|
|
testing.expect(t, array_type.child != cimport.INVALID_TYPE)
|
|
if array_type.child != cimport.INVALID_TYPE {
|
|
testing.expect_value(t, result.types[array_type.child].kind, cimport.Type_Kind.C_Int)
|
|
}
|
|
}
|
|
}
|
|
|
|
typedef_const_array, found_typedef_const_array := find_cimport_variable(
|
|
&result, "imported_typedef_const_array",
|
|
)
|
|
testing.expect(t, found_typedef_const_array)
|
|
if found_typedef_const_array {
|
|
testing.expect(t, !typedef_const_array.mutable)
|
|
testing.expect_value(t, typedef_const_array.reason, "")
|
|
testing.expect(t, typedef_const_array.type != cimport.INVALID_TYPE)
|
|
if typedef_const_array.type != cimport.INVALID_TYPE {
|
|
array_type := result.types[typedef_const_array.type]
|
|
testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array)
|
|
testing.expect(t, !array_type.mutable)
|
|
testing.expect_value(t, array_type.count, u64(2))
|
|
}
|
|
}
|
|
|
|
redeclared_array, found_redeclared_array := find_cimport_variable(
|
|
&result, "imported_redeclared_array",
|
|
)
|
|
testing.expect(t, found_redeclared_array)
|
|
testing.expect_value(t, count_cimport_variables(&result, "imported_redeclared_array"), 1)
|
|
if found_redeclared_array {
|
|
testing.expect_value(t, redeclared_array.reason, "")
|
|
testing.expect(t, redeclared_array.type != cimport.INVALID_TYPE)
|
|
if redeclared_array.type != cimport.INVALID_TYPE {
|
|
array_type := result.types[redeclared_array.type]
|
|
testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array)
|
|
testing.expect_value(t, array_type.count, u64(4))
|
|
}
|
|
}
|
|
|
|
repeated, found_repeated := find_cimport_macro(&result, "IMPORTED_REPEAT")
|
|
testing.expect(t, found_repeated)
|
|
if found_repeated {
|
|
testing.expect_value(t, repeated.value.integer, u64(123))
|
|
}
|
|
testing.expect(t, !cimport_has_named_result(&result, "IMPORTED_GUARDED"))
|
|
testing.expect(t, !cimport_has_named_result(&result, "IMPORTED_ONCE"))
|
|
|
|
negative_decimal, found_negative_decimal := find_cimport_macro(&result, "IMPORTED_NEG_DECIMAL")
|
|
testing.expect(t, found_negative_decimal)
|
|
if found_negative_decimal {
|
|
testing.expect_value(t, result.types[negative_decimal.type].kind, cimport.Type_Kind.C_Long)
|
|
testing.expect_value(t, negative_decimal.value.integer, u64(2147483648))
|
|
testing.expect(t, negative_decimal.value.negative)
|
|
}
|
|
|
|
negative_hex, found_negative_hex := find_cimport_macro(&result, "IMPORTED_NEG_HEX")
|
|
testing.expect(t, found_negative_hex)
|
|
if found_negative_hex {
|
|
testing.expect_value(t, result.types[negative_hex.type].kind, cimport.Type_Kind.C_Uint)
|
|
testing.expect_value(t, negative_hex.value.integer, u64(0x80000000))
|
|
testing.expect(t, !negative_hex.value.negative)
|
|
}
|
|
|
|
negative_uint, found_negative_uint := find_cimport_macro(&result, "IMPORTED_NEG_UINT")
|
|
testing.expect(t, found_negative_uint)
|
|
if found_negative_uint {
|
|
testing.expect_value(t, result.types[negative_uint.type].kind, cimport.Type_Kind.C_Uint)
|
|
testing.expect_value(t, negative_uint.value.integer, u64(0xffffffff))
|
|
testing.expect(t, !negative_uint.value.negative)
|
|
}
|
|
|
|
conversions, found_conversions := find_cimport_macro(&result, "IMPORTED_CONVERSIONS")
|
|
testing.expect(t, found_conversions)
|
|
if found_conversions {
|
|
testing.expect_value(t, len(conversions.values), 5)
|
|
expected_kinds := [?]cimport.Type_Kind{
|
|
.C_Int,
|
|
.C_Double,
|
|
.C_Int,
|
|
.C_Float,
|
|
.C_Int,
|
|
}
|
|
for value, index in conversions.values {
|
|
testing.expect(t, value.type != cimport.INVALID_TYPE)
|
|
if value.type != cimport.INVALID_TYPE {
|
|
testing.expect_value(t, result.types[value.type].kind, expected_kinds[index])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
final_macros_override_same_named_c_value_declarations :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
c_options := cimport.Options{
|
|
include_paths=[]string{"examples/interop/header/include"},
|
|
defines=[]string{"BROLANG_FEATURE"},
|
|
}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header/app",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=c_options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, loaded)
|
|
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_OBJECT = external"))
|
|
testing.expect(t, !strings.contains(llvm_text, "declare i32 @IMPORTED_SHADOW_FUNCTION("))
|
|
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_UNSUPPORTED = external"))
|
|
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_EMPTY_SHADOW = external"))
|
|
}
|
|
|
|
@(test)
|
|
static_inline_c_functions_route_through_generated_trampolines :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
c_options := cimport.Options{
|
|
include_paths=[]string{"examples/interop/header/include"},
|
|
defines=[]string{"BROLANG_FEATURE"},
|
|
}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header/app",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=c_options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, loaded)
|
|
|
|
// Symbols are namespaced by a header-path hash, so match by suffix.
|
|
scalar_symbol := ""
|
|
record_symbol := ""
|
|
for trampoline in module.c_trampolines {
|
|
testing.expect(t, strings.has_prefix(trampoline.symbol, "__brolang_inline_"))
|
|
if strings.has_suffix(trampoline.symbol, "_imported_inline") {
|
|
scalar_symbol = trampoline.symbol
|
|
}
|
|
if strings.has_suffix(trampoline.symbol, "_imported_inline_record") {
|
|
record_symbol = trampoline.symbol
|
|
}
|
|
// The variadic static inline is unsupported and must not be wrapped.
|
|
testing.expect(t, !strings.has_suffix(trampoline.symbol, "_imported_inline_variadic"))
|
|
}
|
|
testing.expect(t, scalar_symbol != "")
|
|
testing.expect(t, record_symbol != "")
|
|
|
|
// A static inline whose signature translates but is rejected by the loader's
|
|
// by-value layout checks keeps its cimport-assigned link_name yet must not
|
|
// emit a wrapper — it is uncallable, so the wrapper would be dead code.
|
|
bad_layout_link := ""
|
|
for function in module.functions {
|
|
if symbol.resolve(&symbols, function.name) == "imported_inline_bad_layout" {
|
|
testing.expect(t, len(function.unsupported_reason) > 0)
|
|
bad_layout_link = function.link_name
|
|
}
|
|
}
|
|
testing.expect(t, bad_layout_link != "") // cimport did generate a wrapper symbol
|
|
for trampoline in module.c_trampolines {
|
|
testing.expect(t, trampoline.symbol != bad_layout_link)
|
|
}
|
|
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, fmt.tprintf("@%s(", scalar_symbol)))
|
|
testing.expect(t, strings.contains(llvm_text, fmt.tprintf("@%s(", record_symbol)))
|
|
// The internal-linkage C symbol itself is never declared or called directly.
|
|
testing.expect(t, !strings.contains(llvm_text, "@imported_inline("))
|
|
}
|
|
|
|
@(test)
|
|
loader_injects_and_caches_cimport_backend_per_compilation :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
state := Fake_Cimport_State{available=true}
|
|
options := cimport.Options{
|
|
include_paths=[]string{"first/include", "second/include"},
|
|
defines=[]string{"FIRST=1", "SECOND"},
|
|
backend={import_header=fake_cimport_backend, user_data=&state},
|
|
}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header_cache",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, state.calls, 1)
|
|
testing.expect(t, state.saw_options)
|
|
testing.expect_value(t, len(module.imports), 2)
|
|
testing.expect_value(t, module.imports[0].target, module.imports[1].target)
|
|
testing.expect_value(t, len(module.functions), 2)
|
|
found_variadic := false
|
|
for function in module.functions {
|
|
found_variadic = found_variadic || function.variadic
|
|
}
|
|
testing.expect(t, found_variadic)
|
|
found_external := false
|
|
found_macro := false
|
|
for global in module.globals {
|
|
name := symbol.resolve(&symbols, global.name)
|
|
found_external = found_external || (name == "fake_global" && global.external && global.writable)
|
|
found_macro = found_macro || name == "FAKE_MAGIC"
|
|
}
|
|
testing.expect(t, found_external)
|
|
testing.expect(t, found_macro)
|
|
}
|
|
|
|
@(test)
|
|
conflicting_external_c_globals_are_diagnosed_and_deduped :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
state := Conflict_Cimport_State{}
|
|
options := cimport.Options{backend={import_header=conflict_cimport_backend, user_data=&state}}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header_conflict",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, state.calls, 2)
|
|
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_conflict := false
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "conflicting external C variable declarations for 'conflict_global'") {
|
|
found_conflict = true
|
|
}
|
|
}
|
|
testing.expect(t, found_conflict)
|
|
testing.expect_value(t, count_substring_occurrences(llvm_text, "@conflict_global = external global"), 1)
|
|
testing.expect(t, strings.contains(llvm_text, "@conflict_global = external global i32"))
|
|
testing.expect(t, !strings.contains(llvm_text, "@conflict_global = external global i64"))
|
|
}
|
|
|
|
@(test)
|
|
external_c_global_and_function_link_name_conflict_is_diagnosed :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
state := Symbol_Conflict_Cimport_State{}
|
|
options := cimport.Options{backend={import_header=symbol_conflict_cimport_backend, user_data=&state}}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header_symbol_conflict",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, state.calls, 2)
|
|
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_conflict := false
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "external C variable 'conflict_symbol' conflicts with a C function declaration") {
|
|
found_conflict = true
|
|
}
|
|
}
|
|
testing.expect(t, found_conflict)
|
|
testing.expect(t, !strings.contains(llvm_text, "@conflict_symbol = external global"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @conflict_symbol()"))
|
|
testing.expect(t, !strings.contains(llvm_text, "load i32, ptr @conflict_symbol"))
|
|
}
|
|
|
|
@(test)
|
|
external_c_global_named_main_is_omitted_for_root_entry_point :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
options := cimport.Options{backend={import_header=main_conflict_cimport_backend}}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header_main_conflict",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, loaded)
|
|
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_conflict := false
|
|
for diagnostic in diagnostics.items {
|
|
found_conflict =
|
|
found_conflict ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"external C variable 'main' conflicts with the program entry point",
|
|
)
|
|
}
|
|
testing.expect(t, found_conflict)
|
|
testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main("), 1)
|
|
testing.expect(t, !strings.contains(llvm_text, "@main = external"))
|
|
}
|
|
|
|
@(test)
|
|
external_c_global_named_main_is_omitted_for_synthesized_entry_point :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
options := cimport.Options{backend={import_header=main_conflict_cimport_backend}}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header_main_conflict_missing",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, loaded)
|
|
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_conflict := false
|
|
found_missing_main := false
|
|
for diagnostic in diagnostics.items {
|
|
found_conflict =
|
|
found_conflict ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"external C variable 'main' conflicts with the program entry point",
|
|
)
|
|
found_missing_main =
|
|
found_missing_main ||
|
|
strings.contains(diagnostic.message, "missing or unusable main function")
|
|
}
|
|
testing.expect(t, found_conflict)
|
|
testing.expect(t, found_missing_main)
|
|
testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main("), 1)
|
|
testing.expect(t, !strings.contains(llvm_text, "@main = external"))
|
|
}
|
|
|
|
@(test)
|
|
external_c_global_named_write_is_omitted_for_compiler_runtime :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
options := cimport.Options{backend={import_header=write_conflict_cimport_backend}}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header_write_conflict",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, loaded)
|
|
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_conflict := false
|
|
for diagnostic in diagnostics.items {
|
|
found_conflict =
|
|
found_conflict ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"external C variable 'write' conflicts with the compiler runtime",
|
|
)
|
|
}
|
|
testing.expect(t, found_conflict)
|
|
testing.expect_value(t, count_substring_occurrences(llvm_text, "declare i64 @write("), 1)
|
|
testing.expect(t, !strings.contains(llvm_text, "@write = external"))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
|
|
}
|
|
|
|
@(test)
|
|
tls_reference_and_const_external_array_assignment_are_diagnosed :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header_unsupported",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=c_options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, loaded)
|
|
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_tls := false
|
|
found_excess_aggregate := false
|
|
found_signed_narrow := false
|
|
found_shadowed_unsupported := false
|
|
found_float_overflow := false
|
|
found_empty_shadow := false
|
|
found_inline_variadic := false
|
|
not_writable_count := 0
|
|
for diagnostic in diagnostics.items {
|
|
found_tls =
|
|
found_tls ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"C declaration 'imported_tls_global' is unavailable: thread-local C variables are not supported",
|
|
)
|
|
found_excess_aggregate =
|
|
found_excess_aggregate ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"C declaration 'IMPORTED_TOO_MANY_COLOR' is unavailable: C macro aggregate initializer is not representable",
|
|
)
|
|
found_signed_narrow =
|
|
found_signed_narrow ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"C declaration 'IMPORTED_SIGNED_NARROW_BAD' is unavailable: C macro aggregate initializer is not representable",
|
|
)
|
|
found_shadowed_unsupported =
|
|
found_shadowed_unsupported ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"C declaration 'IMPORTED_SHADOW_UNSUPPORTED' is unavailable: C macro is not a supported constant",
|
|
)
|
|
found_float_overflow =
|
|
found_float_overflow ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"C declaration 'IMPORTED_FLOAT_OVERFLOW' is unavailable: C macro is not a supported constant",
|
|
)
|
|
found_empty_shadow =
|
|
found_empty_shadow ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"C declaration 'IMPORTED_EMPTY_SHADOW' is unavailable: C macro has no replacement value",
|
|
)
|
|
found_inline_variadic =
|
|
found_inline_variadic ||
|
|
strings.contains(
|
|
diagnostic.message,
|
|
"C declaration 'imported_inline_variadic' is unavailable: variadic static inline C functions are not supported",
|
|
)
|
|
if strings.contains(diagnostic.message, "assignment target is not writable") {
|
|
not_writable_count += 1
|
|
}
|
|
}
|
|
testing.expect(t, found_tls)
|
|
testing.expect(t, found_excess_aggregate)
|
|
testing.expect(t, found_signed_narrow)
|
|
testing.expect(t, found_shadowed_unsupported)
|
|
testing.expect(t, found_float_overflow)
|
|
testing.expect(t, found_empty_shadow)
|
|
testing.expect(t, found_inline_variadic)
|
|
testing.expect(t, not_writable_count >= 5)
|
|
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_EMPTY_SHADOW = external"))
|
|
testing.expect(
|
|
t,
|
|
strings.contains(
|
|
llvm_text,
|
|
"@imported_typedef_const_array = external constant [2 x i32]",
|
|
),
|
|
)
|
|
testing.expect_value(
|
|
t,
|
|
count_substring_occurrences(
|
|
llvm_text,
|
|
"@imported_redeclared_array = external global [4 x i32]",
|
|
),
|
|
1,
|
|
)
|
|
testing.expect(
|
|
t,
|
|
!strings.contains(
|
|
llvm_text,
|
|
"ptr @imported_const_array_record, i64 0",
|
|
),
|
|
)
|
|
testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_UNSUPPORTED = external"))
|
|
}
|
|
|
|
@(test)
|
|
cimport_infrastructure_failure_makes_compilation_unavailable :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
state := Fake_Cimport_State{infrastructure=true}
|
|
options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}}
|
|
module, loaded := loader.load(
|
|
"examples/interop/header_cache",
|
|
&sources,
|
|
&diagnostics,
|
|
&symbols,
|
|
c_options=options,
|
|
)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect(t, !loaded)
|
|
testing.expect_value(t, state.calls, 1)
|
|
testing.expect(t, len(diagnostics.items) > 0)
|
|
}
|
|
|
|
@(test)
|
|
source_store_owns_buffers_indexes_lines_and_deduplicates_diagnostics :: proc(t: ^testing.T) {
|
|
store := source.init_store()
|
|
defer source.destroy_store(&store)
|
|
bytes := make([]byte, len("one\ntwo\n"))
|
|
copy(bytes, "one\ntwo\n")
|
|
source_id := source.add_source_owned(&store, "owned.bro", bytes)
|
|
bytes[0] = 'O'
|
|
|
|
diagnostics := source.init_store_diagnostics(&store)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
span := source.Span{file=source_id, start=4, end=7}
|
|
first := source.add(&diagnostics, span, "same")
|
|
second := source.addf(&diagnostics, span, "%s", "same")
|
|
other := source.add(&diagnostics, span, "other")
|
|
formatted := source.format(&diagnostics, other)
|
|
defer delete(formatted)
|
|
|
|
testing.expect_value(t, store.items[source_id].text, "One\ntwo\n")
|
|
testing.expect_value(t, len(store.items[source_id].line_starts), 3)
|
|
testing.expect_value(t, first, second)
|
|
testing.expect_value(t, len(diagnostics.items), 2)
|
|
testing.expect(t, strings.contains(formatted, "--> owned.bro:2:1"))
|
|
}
|
|
|
|
@(test)
|
|
diagnostic_warnings_format_and_dedupe_by_severity :: proc(t: ^testing.T) {
|
|
source_file := source.Source{path="test.bro", text="one\n"}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
span := source.Span{start=0, end=3}
|
|
|
|
warning := source.add_warning(&diagnostics, span, "same")
|
|
warning_again := source.addf_warning(&diagnostics, span, "%s", "same")
|
|
err := source.add(&diagnostics, span, "same")
|
|
formatted_warning := source.format(&diagnostics, warning)
|
|
defer delete(formatted_warning)
|
|
formatted_error := source.format(&diagnostics, err)
|
|
defer delete(formatted_error)
|
|
|
|
testing.expect_value(t, warning, warning_again)
|
|
testing.expect(t, warning != err)
|
|
testing.expect_value(t, len(diagnostics.items), 2)
|
|
testing.expect_value(t, diagnostics.items[warning].severity, source.Severity.Warning)
|
|
testing.expect_value(t, diagnostics.items[err].severity, source.Severity.Error)
|
|
testing.expect(t, strings.contains(formatted_warning, "warning: same\n --> test.bro:1:1"))
|
|
testing.expect(t, strings.contains(formatted_error, "error: same\n --> test.bro:1:1"))
|
|
}
|
|
|
|
@(test)
|
|
rich_diagnostics_render_labels_notes_help_and_tabs :: proc(t: ^testing.T) {
|
|
store := source.init_store()
|
|
defer source.destroy_store(&store)
|
|
primary_file := source.add_source(&store, "main.bro", "\tmissing()\nnext()\n")
|
|
definition_file := source.add_source(&store, "dep.bro", "value :: 1\n")
|
|
diagnostics := source.init_store_diagnostics(&store)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
id := source.add(&diagnostics, source.Span{file=primary_file, start=1, end=8}, "unknown symbol 'missing'")
|
|
source.set_primary_label(&diagnostics, id, "unknown symbol")
|
|
source.set_primary_label(&diagnostics, id, "unknown symbol")
|
|
source.add_secondary_label(
|
|
&diagnostics, id,
|
|
source.Span{file=definition_file, start=0, end=5},
|
|
"related declaration",
|
|
)
|
|
source.add_note(&diagnostics, id, "names resolve in the current scope")
|
|
source.add_note(&diagnostics, id, "names resolve in the current scope")
|
|
source.add_help(&diagnostics, id, "declare 'missing' before using it")
|
|
formatted := source.format(&diagnostics, id)
|
|
defer delete(formatted)
|
|
multiline := source.add(
|
|
&diagnostics,
|
|
source.Span{file=primary_file, start=1, end=17},
|
|
"multiline failure",
|
|
)
|
|
multiline_formatted := source.format(&diagnostics, multiline)
|
|
defer delete(multiline_formatted)
|
|
unknown := source.add(&diagnostics, source.Span{}, "no location")
|
|
unknown_formatted := source.format(&diagnostics, unknown)
|
|
defer delete(unknown_formatted)
|
|
|
|
testing.expect_value(t, len(diagnostics.annotations), 4)
|
|
testing.expect(t, strings.contains(formatted, "error: unknown symbol 'missing'"))
|
|
testing.expect(t, strings.contains(formatted, "--> main.bro:1:2"))
|
|
testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol"))
|
|
testing.expect(t, strings.contains(formatted, "::: dep.bro:1:1"))
|
|
testing.expect(t, strings.contains(formatted, "----- related declaration"))
|
|
testing.expect(t, strings.contains(formatted, "note: names resolve in the current scope"))
|
|
testing.expect(t, strings.contains(formatted, "help: declare 'missing' before using it"))
|
|
testing.expect(t, strings.contains(multiline_formatted, "1 | missing()"))
|
|
testing.expect(t, !strings.contains(multiline_formatted, "next()"))
|
|
testing.expect_value(t, unknown_formatted, "main.bro: error: no location")
|
|
}
|
|
|
|
@(test)
|
|
poisoned_expressions_preserve_independent_root_diagnostics :: proc(t: ^testing.T) {
|
|
text := `bad :: missing_global
|
|
sink func(value i32) void { _ = value }
|
|
broken func(value int) int { return missing_return + value }
|
|
main func() void {
|
|
_ = missing_add + 1
|
|
_ = try missing_try()
|
|
missing_catch() catch |_| {}
|
|
sink(missing_arg)
|
|
missing_stmt()
|
|
missing_target.field = 1
|
|
value i32 = 0
|
|
value += missing_rhs
|
|
_ = broken(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 9)
|
|
for diagnostic in diagnostics.items {
|
|
testing.expect(t, strings.has_prefix(diagnostic.message, "unknown symbol 'missing_"))
|
|
testing.expect(t, !strings.contains(diagnostic.message, "fallible expression"))
|
|
testing.expect(t, !strings.contains(diagnostic.message, "must be consumed"))
|
|
testing.expect(t, !strings.contains(diagnostic.message, "compatible numeric operands"))
|
|
testing.expect(t, !strings.contains(diagnostic.message, "implicitly convert"))
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
maximum_signed_i64_literal_parses_exactly :: proc(t: ^testing.T) {
|
|
source_file := source.Source{path="test.bro", text="value :: 9223372036854775807\nmain func() void {}\n"}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(9223372036854775807))
|
|
}
|
|
|
|
@(test)
|
|
negative_constants_fold_and_accept_signed_i64_minimum :: proc(t: ^testing.T) {
|
|
text := `minimum :: -9223372036854775808
|
|
grouped i64 :: -(9223372036854775808)
|
|
folded :: -(1 + 2)
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, types.equal(hir_module.globals[0].type, types.I64))
|
|
testing.expect(t, types.equal(hir_module.globals[1].type, types.I64))
|
|
testing.expect(t, types.equal(hir_module.globals[2].type, types.I8))
|
|
testing.expect_value(t, hir_module.globals[0].static_value, i64(-9223372036854775807-1))
|
|
testing.expect_value(t, hir_module.globals[1].static_value, i64(-9223372036854775807-1))
|
|
testing.expect_value(t, hir_module.globals[2].static_value, i64(-3))
|
|
}
|
|
|
|
@(test)
|
|
constant_division_by_zero_has_a_precise_diagnostic :: proc(t: ^testing.T) {
|
|
text := `value :: divtrunc!(5, 0)
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_division_by_zero := false
|
|
found_overflow := false
|
|
for diagnostic in diagnostics.items {
|
|
found_division_by_zero = found_division_by_zero ||
|
|
strings.contains(diagnostic.message, "division builtin denominator is zero")
|
|
found_overflow = found_overflow ||
|
|
strings.contains(diagnostic.message, "integer constant expression exceeds signed i64 range")
|
|
}
|
|
testing.expect(t, found_division_by_zero)
|
|
testing.expect(t, !found_overflow)
|
|
}
|
|
|
|
@(test)
|
|
out_of_range_negative_constants_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := `positive :: 9223372036854775808
|
|
below_minimum :: -9223372036854775809
|
|
double_minimum :: --9223372036854775808
|
|
maximum_u64 :: 18446744073709551615
|
|
beyond_u64 :: 18446744073709551616
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_expression_range := 0
|
|
found_literal_range := 0
|
|
found_u64_range := false
|
|
for diagnostic in diagnostics.items {
|
|
found_expression_range += 1 if strings.contains(diagnostic.message, "exceeds signed i64 range") else 0
|
|
found_literal_range += 1 if strings.contains(diagnostic.message, "does not fit in i64") else 0
|
|
found_u64_range = found_u64_range || strings.contains(diagnostic.message, "magnitude does not fit in u64")
|
|
}
|
|
testing.expect_value(t, found_expression_range, 2)
|
|
testing.expect_value(t, found_literal_range, 2)
|
|
testing.expect(t, found_u64_range)
|
|
}
|
|
|
|
@(test)
|
|
runtime_negation_preserves_operand_type_before_result_widening :: proc(t: ^testing.T) {
|
|
text := `negate_i8 func(value i8) i8 {
|
|
return -value
|
|
}
|
|
widen_after_negate func(value i8) i16 {
|
|
return -value
|
|
}
|
|
main func() void {
|
|
_ = negate_i8(1)
|
|
_ = widen_after_negate(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i8"))
|
|
found_widen := false
|
|
for function in hir_module.functions {
|
|
if symbol.resolve(&symbols, function.name) != "widen_after_negate" {
|
|
continue
|
|
}
|
|
statement := hir_module.statements[function.body[0]]
|
|
widen := hir_module.exprs[statement.expr]
|
|
negate := hir_module.exprs[widen.left]
|
|
testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen)
|
|
testing.expect(t, types.equal(widen.type, types.I16))
|
|
testing.expect_value(t, negate.kind, hir.Expr_Kind.Negate)
|
|
testing.expect(t, types.equal(negate.type, types.I8))
|
|
found_widen = true
|
|
}
|
|
testing.expect(t, found_widen)
|
|
}
|
|
|
|
@(test)
|
|
malformed_hir_references_lower_to_valid_trapped_llvm :: proc(t: ^testing.T) {
|
|
hir_module := hir.init_module()
|
|
defer hir.destroy_module(&hir_module)
|
|
append(&hir_module.exprs, hir.Expr{
|
|
kind=.Local,
|
|
type=types.I8,
|
|
target=hir.local_ref(hir.INVALID_LOCAL),
|
|
left=hir.INVALID_EXPR,
|
|
right=hir.INVALID_EXPR,
|
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
|
})
|
|
append(&hir_module.statements, hir.Stmt{
|
|
kind=.Expression,
|
|
expr=hir.Expr_Id(0),
|
|
local=hir.INVALID_LOCAL,
|
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
|
})
|
|
body := make([]hir.Stmt_Id, 1)
|
|
body[0] = hir.Stmt_Id(0)
|
|
append(&hir_module.functions, hir.Function{
|
|
name=symbol.INVALID,
|
|
link_name=strings.clone("main"),
|
|
calling_convention=.C,
|
|
implementation=.Definition,
|
|
linkage=.External,
|
|
is_main=true,
|
|
result=types.VOID,
|
|
body=body,
|
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
|
})
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
source_file := source.Source{path="test.bro", text=""}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(text)
|
|
|
|
testing.expect(t, strings.contains(text, "call void @bro.trap"))
|
|
testing.expect(t, !strings.contains(text, "%v-1"))
|
|
}
|
|
|
|
@(test)
|
|
malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) {
|
|
module := ir.init_module()
|
|
defer ir.destroy_module(&module)
|
|
instructions := make([]ir.Instruction, 11)
|
|
instructions[0] = ir.Instruction{op=.Store, type=types.I8, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
|
|
instructions[1] = ir.Instruction{op=.Add_Checked, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
|
|
instructions[2] = ir.Instruction{op=.Neg_Checked, type=types.I16, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
|
|
division_ops := [?]ir.Opcode{
|
|
.Div_Checked,
|
|
.Div_Trunc_Checked, .Div_Floor_Checked, .Div_Exact_Checked, .Div_Ceil_Checked,
|
|
.Rem_Checked, .Mod_Checked,
|
|
}
|
|
for op, index in division_ops {
|
|
instructions[3+index] = ir.Instruction{op=op, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
|
|
}
|
|
instructions[10] = ir.Instruction{op=.Return, type=types.I32, a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
|
|
append(&module.functions, ir.Function{
|
|
link_name=strings.clone("main"),
|
|
calling_convention=.C,
|
|
implementation=.Definition,
|
|
linkage=.External,
|
|
is_main=true,
|
|
result=types.I32,
|
|
instructions=instructions,
|
|
})
|
|
source_file := source.Source{path="test.bro", text=""}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
text := llvm.emit(&module, &diagnostics, &symbols)
|
|
defer delete(text)
|
|
|
|
testing.expect(t, strings.contains(text, "call void @bro.trap"))
|
|
testing.expect(t, strings.contains(text, "%v0 = add i8 0, -86"))
|
|
testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766"))
|
|
testing.expect(t, strings.contains(text, "%v2 = add i16 0, -21846"))
|
|
for index in 3..=9 {
|
|
testing.expect(t, strings.contains(text, fmt.tprintf("%%v%d = add i32 0, -1431655766", index)))
|
|
}
|
|
testing.expect(t, strings.contains(text, "@bro.trap(ptr %message, i64 %length) noreturn"))
|
|
testing.expect(t, !strings.contains(text, "%v-1"))
|
|
llvm_path := "/tmp/brolang-test-malformed-recovery.ll"
|
|
output := "/tmp/brolang-test-malformed-recovery"
|
|
defer _ = os.remove(llvm_path)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.write_entire_file(llvm_path, transmute([]byte)text))
|
|
testing.expect(t, backend.compile(llvm_path, output))
|
|
}
|
|
|
|
@(test)
|
|
hundred_thousand_term_runtime_addition_uses_iterative_pipeline :: proc(t: ^testing.T) {
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
strings.write_string(&builder, "sum func(value i32) i32 { return value")
|
|
for _ in 0..<100_000 {
|
|
strings.write_string(&builder, " + 1")
|
|
}
|
|
strings.write_string(&builder, " }\nmain func() void { _ = sum(0) }\n")
|
|
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
instruction_count := 0
|
|
for function in ir_module.functions {
|
|
instruction_count += len(function.instructions)
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, instruction_count > 100_000)
|
|
}
|
|
|
|
@(test)
|
|
global_cycle_diagnostic_reports_endpoint_lines :: proc(t: ^testing.T) {
|
|
text := `a i32 :: b
|
|
b i32 :: a
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for _, diagnostic_index in diagnostics.items {
|
|
message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index))
|
|
found = found || strings.contains(message, "global initialization cycle from 'a' at test.bro:1 to 'b' at test.bro:2")
|
|
delete(message)
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
deep_global_cycle_detection_uses_iterative_dfs :: proc(t: ^testing.T) {
|
|
count := 50_000
|
|
ast_module := ast.init_module()
|
|
defer ast.destroy_module(&ast_module)
|
|
source_file := source.Source{path="test.bro", text=""}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
name := symbol.intern(&symbols, "value")
|
|
state := checker.Checker{
|
|
ast_module=&ast_module,
|
|
diagnostics=&diagnostics,
|
|
symbols=&symbols,
|
|
module=hir.init_module(),
|
|
allocator=context.allocator,
|
|
}
|
|
defer hir.destroy_module(&state.module)
|
|
defer delete(state.cycle_stack)
|
|
|
|
for id in 0..<count {
|
|
append(&ast_module.globals, ast.Global{name=name})
|
|
dependencies: [dynamic]hir.Global_Id
|
|
dependencies.allocator = context.allocator
|
|
append(&dependencies, hir.global_id((id+1)%count))
|
|
append(&state.module.globals, hir.Global{name=name, dependencies=dependencies})
|
|
}
|
|
states := make([]u8, count)
|
|
defer delete(states)
|
|
checker.detect_global_cycles_visit(&state, hir.Global_Id(0), states)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
for global in state.module.globals {
|
|
testing.expect(t, global.problematic)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
mutable_local_reassignment_uses_runtime_storage :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-mutable"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/mutable_local", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 3)
|
|
}
|
|
|
|
@(test)
|
|
implicit_narrowing_produces_trap_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-narrowing"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/narrowing_error", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
valid_runtime_global_initializes_before_main :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-runtime-global"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/runtime_global", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
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 }
|
|
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_values := false
|
|
for global 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 == "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_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
|
|
bad i32 = undefined
|
|
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
|
|
undefined_global := 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")
|
|
undefined_global = undefined_global || strings.contains(diagnostic.message, "'undefined' is only valid as a mutable local declaration initializer")
|
|
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, undefined_global)
|
|
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)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
used_global_cycle_traps :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-cycle-used"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/cycle_used", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
malformed_typed_values_still_produce_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-malformed-typed"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/malformed_typed_recovery", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
unused_invalid_function_body_is_not_checked :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-invalid-unused-function"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/invalid_unused_function", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
used_invalid_function_is_diagnosed_and_traps :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-invalid-used-function"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/invalid_used_function", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
function_mediated_problematic_global_is_deferred :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-function-global-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/function_global_unused", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
function_mediated_problematic_global_traps_when_used :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-function-global-used"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/function_global_used", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
package_files_merge_and_qualified_imports_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-basic"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/basic/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 3)
|
|
}
|
|
|
|
@(test)
|
|
unused_import_is_diagnosed_but_remains_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/unused/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
unused_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)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
referenced_missing_package_traps_at_reference :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-missing-used"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/missing_used/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}},
|
|
context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
testing.expect(t, !state.success)
|
|
testing.expect(t, strings.contains(string(stderr), "/missing_used/app/main.bro:4:6"))
|
|
}
|
|
|
|
@(test)
|
|
imports_are_file_local :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-file-local"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/file_local/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
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_file_hidden :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-hidden-declarations"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/hidden_valid/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 9)
|
|
}
|
|
|
|
@(test)
|
|
file_hidden_declarations_are_not_package_members :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
module, loaded := loader.load("examples/packages/hidden_invalid/app", &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&module)
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect(t, loaded)
|
|
found_sibling := false
|
|
found_sibling_value := false
|
|
found_sibling_type := false
|
|
found_import := false
|
|
found_collision := false
|
|
for diagnostic in diagnostics.items {
|
|
found_sibling = found_sibling || strings.contains(diagnostic.message, "unknown symbol 'sibling'")
|
|
found_sibling_value = found_sibling_value || strings.contains(diagnostic.message, "unknown symbol 'sibling_value'")
|
|
found_sibling_type = found_sibling_type || strings.contains(diagnostic.message, "unknown or opaque record type 'Sibling'")
|
|
found_import = found_import || strings.contains(diagnostic.message, "package 'dep' has no member 'secret'")
|
|
found_collision = found_collision || strings.contains(diagnostic.message, "duplicate function 'collision'")
|
|
}
|
|
testing.expect(t, found_sibling)
|
|
testing.expect(t, found_sibling_value)
|
|
testing.expect(t, found_sibling_type)
|
|
testing.expect(t, found_import)
|
|
testing.expect(t, found_collision)
|
|
}
|
|
|
|
@(test)
|
|
package_import_cycles_are_valid :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-cycle"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/cycle/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 4)
|
|
}
|
|
|
|
@(test)
|
|
imported_main_is_an_ordinary_callable_function :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-imported-main"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/imported_main/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 7)
|
|
}
|
|
|
|
@(test)
|
|
same_named_c_functions_in_packages_do_not_collide :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-c-symbols"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/c_symbols/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
invalid_root_package_preserves_existing_output :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-preserved-output"
|
|
defer _ = os.remove(output)
|
|
previous := "previous artifact"
|
|
testing.expect(t, os.write_entire_file(output, transmute([]byte)previous))
|
|
testing.expect_value(t, compiler_core.compile_package("examples/programs/prototype/main.bro", output), 2)
|
|
data, ok := os.read_entire_file(output)
|
|
defer delete(data)
|
|
testing.expect(t, ok)
|
|
testing.expect_value(t, string(data), previous)
|
|
}
|
|
|
|
@(test)
|
|
empty_root_package_is_an_infrastructure_error :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-empty"
|
|
defer _ = os.remove(output)
|
|
testing.expect_value(t, compiler_core.compile_package("examples/packages/empty", output), 2)
|
|
testing.expect(t, !os.exists(output))
|
|
}
|
|
|
|
@(test)
|
|
same_package_can_be_imported_under_distinct_aliases :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-aliases"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/aliases/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 4)
|
|
}
|
|
|
|
@(test)
|
|
duplicate_same_file_import_keeps_first_binding :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-duplicate"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/duplicate/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 2)
|
|
}
|
|
|
|
@(test)
|
|
identical_imports_in_different_files_are_independent :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-repeated"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/repeated/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 4)
|
|
}
|
|
|
|
@(test)
|
|
imports_do_not_reexport_members :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-non-transitive"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/non_transitive/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
declaration_aliases_preserve_identity_and_chain :: proc(t: ^testing.T) {
|
|
root :: "/tmp/brolang-test-declaration-aliases"
|
|
dep_dir :: root + "/dep"
|
|
facade_dir :: root + "/facade"
|
|
top_dir :: root + "/top"
|
|
app_dir :: root + "/app"
|
|
output :: "/tmp/brolang-test-declaration-aliases-output"
|
|
_ = os2.remove_all(root)
|
|
defer _ = os2.remove_all(root)
|
|
defer _ = os.remove(output)
|
|
directories := [?]string{root, dep_dir, facade_dir, top_dir, app_dir}
|
|
for directory in directories {
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
}
|
|
dep_text := `Box func($T type) type {
|
|
return struct { value T }
|
|
}
|
|
Point :: struct { value i32 }
|
|
counter i32 = 1
|
|
answer func() i32 { return 40 }
|
|
`
|
|
facade_text := `dep :: import "../dep"
|
|
RenamedBox :: alias dep.Box
|
|
RenamedPoint :: alias dep.Point
|
|
counter :: alias dep.counter
|
|
answer :: alias dep.answer
|
|
hide local_answer_alias :: alias dep.answer
|
|
local_answer func() i32 { return local_answer_alias() }
|
|
Scalar :: alias i32
|
|
MaybePoint :: alias ?@dep.Point
|
|
Concrete :: alias dep.Box(i32)
|
|
`
|
|
top_text := `facade :: import "../facade"
|
|
Box :: alias facade.RenamedBox
|
|
Point :: alias facade.RenamedPoint
|
|
counter :: alias facade.counter
|
|
answer :: alias facade.answer
|
|
`
|
|
app_text := `dep :: import "../dep"
|
|
facade :: import "../facade"
|
|
top :: import "../top"
|
|
|
|
main func() i32 {
|
|
box top.Box(i32) :: top.Box(i32) { value = 2 }
|
|
point top.Point :: top.Point { value = 3 }
|
|
maybe facade.MaybePoint :: none
|
|
scalar facade.Scalar :: 5
|
|
top.counter = 7
|
|
if box.value != 2 or point.value != 3 { return 1 }
|
|
if scalar != 5 or top.answer() != 40 or facade.local_answer() != 40 or dep.counter != 7 { return 2 }
|
|
_ = maybe
|
|
return 0
|
|
}
|
|
`
|
|
testing.expect(t, os.write_entire_file(dep_dir + "/dep.bro", transmute([]byte)dep_text))
|
|
testing.expect(t, os.write_entire_file(facade_dir + "/facade.bro", transmute([]byte)facade_text))
|
|
testing.expect(t, os.write_entire_file(top_dir + "/top.bro", transmute([]byte)top_text))
|
|
testing.expect(t, os.write_entire_file(app_dir + "/main.bro", transmute([]byte)app_text))
|
|
|
|
status := compiler_core.compile_package(app_dir, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
declaration_aliases_diagnose_invalid_targets :: proc(t: ^testing.T) {
|
|
root :: "/tmp/brolang-test-declaration-alias-errors"
|
|
dep_dir :: root + "/dep"
|
|
facade_dir :: root + "/facade"
|
|
a_dir :: root + "/a"
|
|
b_dir :: root + "/b"
|
|
app_dir :: root + "/app"
|
|
_ = os2.remove_all(root)
|
|
defer _ = os2.remove_all(root)
|
|
directories := [?]string{root, dep_dir, facade_dir, a_dir, b_dir, app_dir}
|
|
for directory in directories {
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
}
|
|
dep_text := `visible func() i32 { return 1 }
|
|
hide hidden_target func() i32 { return 2 }
|
|
ambiguous func() i32 { return 3 }
|
|
ambiguous i32 :: 4
|
|
`
|
|
facade_text := `dep :: import "../dep"
|
|
gone :: import "../gone"
|
|
missing :: alias dep.missing
|
|
hidden :: alias dep.hidden_target
|
|
unknown :: alias nope.visible
|
|
unavailable :: alias gone.visible
|
|
ambiguous :: alias dep.ambiguous
|
|
duplicate :: alias dep.visible
|
|
duplicate :: alias dep.visible
|
|
collision func() i32 { return 0 }
|
|
collision :: alias dep.visible
|
|
dep :: alias dep.visible
|
|
`
|
|
a_text := `b :: import "../b"
|
|
value :: alias b.value
|
|
`
|
|
b_text := `a :: import "../a"
|
|
value :: alias a.value
|
|
`
|
|
app_text := `facade :: import "../facade"
|
|
a :: import "../a"
|
|
main func() void {}
|
|
`
|
|
testing.expect(t, os.write_entire_file(dep_dir + "/dep.bro", transmute([]byte)dep_text))
|
|
testing.expect(t, os.write_entire_file(facade_dir + "/facade.bro", transmute([]byte)facade_text))
|
|
testing.expect(t, os.write_entire_file(a_dir + "/a.bro", transmute([]byte)a_text))
|
|
testing.expect(t, os.write_entire_file(b_dir + "/b.bro", transmute([]byte)b_text))
|
|
testing.expect(t, os.write_entire_file(app_dir + "/main.bro", transmute([]byte)app_text))
|
|
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
module, loaded := loader.load(app_dir, &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, loaded)
|
|
wants := []string{
|
|
"has no member 'missing'",
|
|
"is file-hidden",
|
|
"unknown symbol 'nope'",
|
|
"unavailable imported package 'gone'",
|
|
"package member 'dep.ambiguous' is ambiguous",
|
|
"duplicate declaration alias 'duplicate'",
|
|
"declaration alias 'collision' conflicts with a package declaration",
|
|
"declaration alias 'dep' conflicts with an import",
|
|
"declaration alias cycle",
|
|
}
|
|
for want in wants {
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, want)
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
self_import_via_dot_is_valid :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-self"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/self_import/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 5)
|
|
}
|
|
|
|
@(test)
|
|
unused_absolute_import_is_diagnosed_without_trapping :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-absolute"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/absolute/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
unreferenced_problematic_imported_global_is_deferred :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-problematic-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/problematic_unused/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
valid_eager_global_in_unused_package_still_initializes :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-eager-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/eager_unused/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
cross_package_global_initialization_cycle_traps_when_used :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-global-cycle"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/global_cycle/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
cross_package_generic_specializes_from_folded_argument :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-generic"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/generic/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 128)
|
|
}
|
|
|
|
@(test)
|
|
comptime_value_params_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-comptime-value-params"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/comptime_value_params", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
comptime_type_params_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-comptime-type-params"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/comptime_type_params", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
type_factories_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-type-factory"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/type_factory", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
arraylist_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-arraylist"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/arraylist", output, nil, target.DEFAULT, cimport.Options{}, ".")
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
type_factory_rejects_runtime_parameters_and_recursion :: proc(t: ^testing.T) {
|
|
texts := []string{
|
|
`Bad func($T type, n usize) type {
|
|
return struct { value [n]T }
|
|
}
|
|
main func() void { value Bad(i32, 4) = undefined; _ = &value }
|
|
`,
|
|
`Loop func($T type) type {
|
|
return struct { next @Loop(T) }
|
|
}
|
|
main func() void { value Loop(i32) = undefined; _ = &value }
|
|
`,
|
|
`Box func($T type) type {
|
|
return struct { value T }
|
|
}
|
|
main func() void { _ = Box(i32) }
|
|
`,
|
|
`Bad func($T type) type {
|
|
return 1
|
|
}
|
|
main func() void { value Bad(i32) = undefined; _ = &value }
|
|
`,
|
|
}
|
|
wanted := []string{"must be comptime", "recursive type-factory specialization", "only valid in type position", "cannot implicitly convert"}
|
|
for text, index in texts {
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
symbols := symbol.init_table()
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, wanted[index])
|
|
}
|
|
testing.expect(t, found)
|
|
hir.destroy_module(&hir_module)
|
|
ast.destroy_module(&ast_module)
|
|
delete(stream.items)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
}
|
|
|
|
}
|
|
|
|
@(test)
|
|
comptime_eval_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-comptime-eval"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/comptime_eval", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
comptime_v1_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-comptime-v1"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/comptime_v1", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
lazy_function_body_marks_import_as_used_without_resolving_it :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-lazy-import"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/lazy_import/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
qualified_global_inference_ignores_same_named_local :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-qualified-shadow"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/qualified_shadow/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 300)
|
|
}
|
|
|
|
@(test)
|
|
cross_package_recursive_specialization_reaches_fixed_point :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-recursive"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/recursive/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
testing.expect(t, os.exists(output))
|
|
}
|
|
|
|
@(test)
|
|
imported_main_does_not_satisfy_root_main_requirement :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-missing-root-main"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/missing_root_main/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
package_llvm_is_deterministic_and_symbols_include_package_ids :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load("examples/packages/c_symbols/app", &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
second_llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(second_llvm_text)
|
|
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, llvm_text, second_llvm_text)
|
|
testing.expect(t, strings.contains(llvm_text, "define signext i8 @bro_c__p1__same"))
|
|
testing.expect(t, strings.contains(llvm_text, "define signext i8 @bro_c__p2__same"))
|
|
testing.expect(t, strings.contains(llvm_text, "define i32 @main()"))
|
|
}
|
|
|
|
@(test)
|
|
invalid_default_alias_requires_an_explicit_alias :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-default-alias-invalid"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/default_alias_invalid/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
explicit_alias_allows_invalid_directory_basename :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-explicit-alias"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/explicit_alias/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 3)
|
|
}
|
|
|
|
@(test)
|
|
import_alias_conflict_is_diagnosed_without_trapping :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-alias-conflict"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/alias_conflict/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 7)
|
|
}
|
|
|
|
@(test)
|
|
empty_imported_package_is_a_source_diagnostic :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-empty-import"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/empty_import/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
non_directory_import_is_a_source_diagnostic :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-file-import"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/file_import/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
import_is_available_before_its_textual_declaration :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-import-order"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/import_order/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 6)
|
|
}
|
|
|
|
@(test)
|
|
deeply_nested_child_packages_load_recursively :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-deep"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/deep/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 9)
|
|
}
|
|
|
|
@(test)
|
|
function_returning_only_in_if_branch_is_diagnosed :: proc(t: ^testing.T) {
|
|
text := `classify func(n i32) i32 {
|
|
if n > 0 {
|
|
return 1
|
|
}
|
|
}
|
|
main func() void {
|
|
_ = classify(5)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "does not return a value")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
function_returning_in_both_if_arms_is_accepted :: proc(t: ^testing.T) {
|
|
text := `classify func(n i32) i32 {
|
|
if n > 0 {
|
|
return 1
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
main func() void {
|
|
_ = classify(5)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "does not return a value")
|
|
}
|
|
testing.expect(t, !found)
|
|
}
|
|
|
|
@(test)
|
|
function_returning_after_if_is_accepted :: proc(t: ^testing.T) {
|
|
text := `classify func(n i32) i32 {
|
|
if n > 0 {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
main func() void {
|
|
_ = classify(5)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "does not return a value")
|
|
}
|
|
testing.expect(t, !found)
|
|
}
|
|
|
|
@(test)
|
|
conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-conditional-unwrap"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/conditional_unwrap", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
// Single unwrap, guarded two/three-value unwraps, optional pointers, false
|
|
// guards, and failed short-circuit chains preserve the expected total.
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
conditional_unwrap_parser_captures_guard_and_parenthesized_chain :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
first ?i32 = 1
|
|
second ?i32 = 2
|
|
if (first and second) |a, b : a == 1 and b == 2| {
|
|
_ = a
|
|
_ = b
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
statement := module.statements[module.functions[0].body[2]]
|
|
testing.expect_value(t, statement.kind, ast.Stmt_Kind.If)
|
|
testing.expect_value(t, len(statement.captures), 2)
|
|
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.And)
|
|
testing.expect(t, module.exprs[statement.expr].parenthesized)
|
|
testing.expect(t, statement.guard != ast.INVALID_EXPR)
|
|
testing.expect_value(t, module.exprs[statement.guard].kind, ast.Expr_Kind.And)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_braceless_if_bodies :: proc(t: ^testing.T) {
|
|
text := `ready func() bool { return true }
|
|
main func() void {
|
|
x i32 = 0
|
|
if (x == 0) x = 1
|
|
if ready() x = 2
|
|
if (x == 2) x = 3 else x = 4
|
|
if (x > 0) { x = 10 } else x = 11
|
|
v ?i32 = 5
|
|
if (v) |u| _ = u
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
main := module.functions[1]
|
|
testing.expect_value(t, len(main.body), 7)
|
|
|
|
paren_if := module.statements[main.body[1]]
|
|
testing.expect_value(t, paren_if.kind, ast.Stmt_Kind.If)
|
|
testing.expect_value(t, len(paren_if.body), 1)
|
|
testing.expect(t, module.exprs[paren_if.expr].parenthesized)
|
|
|
|
call_if := module.statements[main.body[2]]
|
|
testing.expect_value(t, call_if.kind, ast.Stmt_Kind.If)
|
|
testing.expect_value(t, len(call_if.body), 1)
|
|
testing.expect_value(t, module.exprs[call_if.expr].kind, ast.Expr_Kind.Call)
|
|
|
|
if_else := module.statements[main.body[3]]
|
|
testing.expect_value(t, len(if_else.body), 1)
|
|
testing.expect_value(t, len(if_else.else_body), 1)
|
|
|
|
braced_then := module.statements[main.body[4]]
|
|
testing.expect_value(t, len(braced_then.body), 1)
|
|
testing.expect_value(t, len(braced_then.else_body), 1)
|
|
|
|
unwrap_if := module.statements[main.body[6]]
|
|
testing.expect_value(t, len(unwrap_if.captures), 1)
|
|
testing.expect_value(t, len(unwrap_if.body), 1)
|
|
}
|
|
|
|
@(test)
|
|
parser_diagnoses_braceless_if_without_parens_or_call :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
x i32 = 0
|
|
if x == 0 x = 1
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
testing.expect(t, strings.contains(diagnostics.items[0].message, "parenthesized"))
|
|
}
|
|
|
|
@(test)
|
|
parser_diagnoses_braceless_for_and_unwrap_without_parens_or_call :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
for tokens.items |item| _ = item
|
|
value ?i32 = 1
|
|
if value |present| _ = present
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 2)
|
|
testing.expect(t, strings.contains(diagnostics.items[0].message, "brace-less 'for'"))
|
|
testing.expect(t, strings.contains(diagnostics.items[1].message, "brace-less 'if'"))
|
|
}
|
|
|
|
@(test)
|
|
braceless_if_compiles_and_runs :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-braceless-if"
|
|
main_path := "/tmp/brolang-test-braceless-if/main.bro"
|
|
output := "/tmp/brolang-test-braceless-if-output"
|
|
text := `ready func() bool { return true }
|
|
main func() i32 {
|
|
x i32 = 0
|
|
if (x == 0) x = 1 else x = 2
|
|
if ready() x = x + 10
|
|
y i32 = 5
|
|
if (y == 0) y = 1 else y = 30
|
|
x = x + y
|
|
return x
|
|
}
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
status := compiler_core.compile_package(directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 41)
|
|
}
|
|
|
|
@(test)
|
|
conditional_unwrap_allows_sink_captures :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
first ?i32 = 1
|
|
second ?i32 = 2
|
|
if first and second |_, value : value == 2| {
|
|
_ = value
|
|
}
|
|
if first |_| {}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
main := hir_module.functions[0]
|
|
first_if := hir_module.statements[main.body[2]]
|
|
second_if := hir_module.statements[main.body[3]]
|
|
testing.expect_value(t, first_if.unwraps[0].local, hir.INVALID_LOCAL)
|
|
testing.expect(t, first_if.unwraps[1].local != hir.INVALID_LOCAL)
|
|
testing.expect_value(t, second_if.unwraps[0].local, hir.INVALID_LOCAL)
|
|
}
|
|
|
|
@(test)
|
|
parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^testing.T) {
|
|
cases := [4]struct {
|
|
text: string,
|
|
needle: string,
|
|
}{
|
|
{`main func() void {
|
|
value ?i32 = 1
|
|
if value || {}
|
|
}
|
|
`, "expected an unwrap capture name"},
|
|
{`main func() void {
|
|
value ?i32 = 1
|
|
if value |capture,| {}
|
|
}
|
|
`, "expected an unwrap capture after ','"},
|
|
{`main func() void {
|
|
value ?i32 = 1
|
|
if value |capture :| {}
|
|
}
|
|
`, "expected a guard expression after ':'"},
|
|
{`main func() void {
|
|
value ?i32 = 1
|
|
if value |capture {}
|
|
}
|
|
`, "expected '|' to close unwrap captures"},
|
|
}
|
|
for test_case in cases {
|
|
source_file := source.Source{path="test.bro", text=test_case.text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
symbols := symbol.init_table()
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, test_case.needle)
|
|
}
|
|
testing.expect(t, found)
|
|
|
|
ast.destroy_module(&module)
|
|
delete(stream.items)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
}
|
|
|
|
}
|
|
|
|
@(test)
|
|
if_unwrap_on_non_optional_is_diagnosed :: proc(t: ^testing.T) {
|
|
text := `main func() i32 {
|
|
x i32 = 5
|
|
if x |v| {
|
|
return v
|
|
}
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "unwrap requires an optional")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
conditional_unwrap_diagnostics_cover_counts_guards_and_capture_scope :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
first ?i32 = 1
|
|
second ?i32 = 2
|
|
plain i32 = 3
|
|
|
|
if first and second |one| {}
|
|
if first |one, two| {}
|
|
if first and second |same, same| {}
|
|
if plain |value| {}
|
|
if first |value : value| {}
|
|
if first and earlier |earlier, later| {}
|
|
|
|
if first |value| {
|
|
value = 2
|
|
}
|
|
if first |value| {
|
|
value i32 = 2
|
|
_ = value
|
|
}
|
|
if first |value| {
|
|
_ = value
|
|
} else {
|
|
_ = value
|
|
}
|
|
_ = value
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
count_mismatches := 0
|
|
duplicate := false
|
|
non_optional := false
|
|
guard := false
|
|
outer_operand_scope := false
|
|
immutable := false
|
|
redeclaration := false
|
|
capture_scope := 0
|
|
for diagnostic in diagnostics.items {
|
|
count_mismatches += 1 if strings.contains(diagnostic.message, "unwrap has") else 0
|
|
duplicate = duplicate || strings.contains(diagnostic.message, "unwrap captures must have distinct names")
|
|
non_optional = non_optional || strings.contains(diagnostic.message, "unwrap requires an optional value")
|
|
guard = guard || strings.contains(diagnostic.message, "unwrap guard must be a bool")
|
|
outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unknown symbol 'earlier'")
|
|
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'")
|
|
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'value'")
|
|
capture_scope += 1 if strings.contains(diagnostic.message, "unknown symbol 'value'") else 0
|
|
}
|
|
testing.expect_value(t, count_mismatches, 2)
|
|
testing.expect(t, duplicate)
|
|
testing.expect(t, non_optional)
|
|
testing.expect(t, guard)
|
|
testing.expect(t, outer_operand_scope)
|
|
testing.expect(t, immutable)
|
|
testing.expect(t, redeclaration)
|
|
testing.expect_value(t, capture_scope, 2)
|
|
}
|
|
|
|
@(test)
|
|
if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) {
|
|
// The binding `v` is usable in the then-block but not in the else-block.
|
|
text := `main func() i32 {
|
|
a ?i32 = 1
|
|
if a |v| {
|
|
return v
|
|
} else {
|
|
return v
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "unknown symbol 'v'")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
if_unwrap_binding_is_immutable :: proc(t: ^testing.T) {
|
|
text := `main func() i32 {
|
|
a ?i32 = 1
|
|
if a |v| {
|
|
v = 2
|
|
return v
|
|
}
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "cannot assign immutable local 'v'")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
while_loops_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-while-loop"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/while_loop", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
while_loop_diagnostics_cover_condition_update_and_scope :: proc(t: ^testing.T) {
|
|
text := `bad_condition func() void {
|
|
while 1 {}
|
|
}
|
|
bad_unresolved func() void {
|
|
while false : missing = 1 {}
|
|
}
|
|
bad_immutable func() void {
|
|
i :: 0
|
|
while false : i = i + 1 {}
|
|
}
|
|
bad_body_scope func() void {
|
|
running :: false
|
|
while running : i = 1 {
|
|
i u32 = 0
|
|
}
|
|
}
|
|
bad_declaration_update func() void {
|
|
while false : i u32 = 0 {}
|
|
}
|
|
bad_missing_update func() void {
|
|
while false : {}
|
|
}
|
|
main func() void {
|
|
bad_condition()
|
|
bad_unresolved()
|
|
bad_immutable()
|
|
bad_body_scope()
|
|
bad_declaration_update()
|
|
bad_missing_update()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
non_bool := false
|
|
unresolved_missing := false
|
|
unresolved_body_local := false
|
|
immutable := false
|
|
disallowed := false
|
|
missing := false
|
|
for diagnostic in diagnostics.items {
|
|
non_bool = non_bool || strings.contains(diagnostic.message, "'while' condition must be a bool")
|
|
unresolved_missing = unresolved_missing || strings.contains(diagnostic.message, "cannot assign unresolved local 'missing'")
|
|
unresolved_body_local = unresolved_body_local || strings.contains(diagnostic.message, "cannot assign unresolved local 'i'")
|
|
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'i'")
|
|
disallowed = disallowed || strings.contains(diagnostic.message, "while update must be an assignment, sink, or expression statement")
|
|
missing = missing || strings.contains(diagnostic.message, "expected a while update statement after ':'")
|
|
}
|
|
testing.expect(t, non_bool)
|
|
testing.expect(t, unresolved_missing)
|
|
testing.expect(t, unresolved_body_local)
|
|
testing.expect(t, immutable)
|
|
testing.expect(t, disallowed)
|
|
testing.expect(t, missing)
|
|
}
|
|
|
|
@(test)
|
|
while_true_and_potential_fallthrough_have_distinct_return_analysis :: proc(t: ^testing.T) {
|
|
text := `forever func() i32 {
|
|
while true {}
|
|
}
|
|
maybe func(run bool) i32 {
|
|
while run {
|
|
return 1
|
|
}
|
|
}
|
|
main func() void {
|
|
if false {
|
|
_ = forever()
|
|
}
|
|
_ = maybe(false)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
missing_return_count := 0
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "does not return a value") {
|
|
missing_return_count += 1
|
|
}
|
|
}
|
|
testing.expect_value(t, missing_return_count, 1)
|
|
}
|
|
|
|
@(test)
|
|
while_loop_allocas_are_emitted_in_the_entry_block :: proc(t: ^testing.T) {
|
|
text := `main func() i32 {
|
|
i u32 = 0
|
|
while i < 2 and true : i = i + 1 {
|
|
value u32 = i
|
|
_ = value
|
|
}
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
|
|
testing.expect(t, first_loop_label >= 0)
|
|
alloca_count := 0
|
|
for function in ir_module.functions {
|
|
if !function.is_main {
|
|
continue
|
|
}
|
|
for instruction, instruction_index in function.instructions {
|
|
if instruction.op != .Alloca {
|
|
continue
|
|
}
|
|
alloca_count += 1
|
|
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
|
|
offset := find_substring_offset(llvm_text, needle)
|
|
testing.expect(t, offset >= 0 && offset < first_loop_label)
|
|
}
|
|
}
|
|
testing.expect(t, alloca_count >= 3)
|
|
}
|
|
|
|
@(test)
|
|
for_loop_tokens_and_parser_capture_range_shape :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
for 0..4 |value| {
|
|
_ = value
|
|
}
|
|
items [1]mut i32 = [1]
|
|
for (&items) |@item, index| {
|
|
_ = item
|
|
_ = index
|
|
}
|
|
for 0..=1 |inclusive| {
|
|
_ = inclusive
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
|
|
for_count := 0
|
|
range_count := 0
|
|
inclusive_count := 0
|
|
for tok in stream.items {
|
|
#partial switch tok.kind {
|
|
case .Keyword_For: for_count += 1
|
|
case .Range: range_count += 1
|
|
case .Range_Inclusive: inclusive_count += 1
|
|
case:
|
|
}
|
|
}
|
|
testing.expect_value(t, for_count, 3)
|
|
testing.expect_value(t, range_count, 1)
|
|
testing.expect_value(t, inclusive_count, 1)
|
|
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
function := module.functions[0]
|
|
first := module.statements[function.body[0]]
|
|
second := module.statements[function.body[2]]
|
|
third := module.statements[function.body[3]]
|
|
testing.expect_value(t, first.kind, ast.Stmt_Kind.For)
|
|
testing.expect(t, !first.pointer_capture)
|
|
testing.expect_value(t, module.exprs[first.expr].kind, ast.Expr_Kind.Range)
|
|
testing.expect_value(t, module.exprs[first.expr].integer, u64(0))
|
|
testing.expect_value(t, second.kind, ast.Stmt_Kind.For)
|
|
testing.expect(t, second.pointer_capture)
|
|
testing.expect(t, symbol.is_valid(second.index_name))
|
|
testing.expect_value(t, third.kind, ast.Stmt_Kind.For)
|
|
testing.expect_value(t, module.exprs[third.expr].integer, u64(1))
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_braceless_for_bodies :: proc(t: ^testing.T) {
|
|
text := `make_items func() [2]i32 { return [1, 2] }
|
|
main func() void {
|
|
for (tokens.items) |item| _ = item
|
|
for make_items() |item|
|
|
_ = item
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
main := module.functions[1]
|
|
testing.expect_value(t, len(main.body), 2)
|
|
parenthesized := module.statements[main.body[0]]
|
|
call := module.statements[main.body[1]]
|
|
testing.expect_value(t, len(parenthesized.body), 1)
|
|
testing.expect(t, module.exprs[parenthesized.expr].parenthesized)
|
|
testing.expect_value(t, len(call.body), 1)
|
|
testing.expect_value(t, module.exprs[call.expr].kind, ast.Expr_Kind.Call)
|
|
}
|
|
|
|
@(test)
|
|
braceless_for_compiles_and_runs :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-braceless-for"
|
|
main_path := "/tmp/brolang-test-braceless-for/main.bro"
|
|
output := "/tmp/brolang-test-braceless-for-output"
|
|
text := `make_items func() [2]i32 { return [20, 2] }
|
|
main func() i32 {
|
|
total i32 = 0
|
|
items :: [10, 11]
|
|
for (items) |item| total += item
|
|
for make_items() |item|
|
|
total += item
|
|
return total
|
|
}
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
status := compiler_core.compile_package(directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 43)
|
|
}
|
|
|
|
@(test)
|
|
range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
limit :: 3
|
|
for 0..limit + 1 |bad| {
|
|
_ = bad
|
|
}
|
|
for 0..(limit + 1) |good| {
|
|
_ = good
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found := 0
|
|
for diagnostic in diagnostics.items {
|
|
found += 1 if strings.contains(diagnostic.message, "range bounds with operators must be parenthesized") else 0
|
|
}
|
|
testing.expect_value(t, found, 1)
|
|
}
|
|
|
|
@(test)
|
|
parser_diagnoses_malformed_for_captures :: proc(t: ^testing.T) {
|
|
cases := [4]struct {
|
|
text: string,
|
|
needle: string,
|
|
}{
|
|
{`main func() void {
|
|
for [1] item {}
|
|
}
|
|
`, "expected '|' before for-loop captures"},
|
|
{`main func() void {
|
|
for [1] |@| {}
|
|
}
|
|
`, "expected a for-loop item capture"},
|
|
{`main func() void {
|
|
for [1] |item,| {}
|
|
}
|
|
`, "expected an index capture after ','"},
|
|
{`main func() void {
|
|
for [1] |item {}
|
|
}
|
|
`, "expected '|' to close for-loop captures"},
|
|
}
|
|
for test_case in cases {
|
|
source_file := source.Source{path="test.bro", text=test_case.text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
symbols := symbol.init_table()
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, test_case.needle)
|
|
}
|
|
testing.expect(t, found)
|
|
|
|
ast.destroy_module(&module)
|
|
delete(stream.items)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
for_loops_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-for-loop"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/for_loop", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
range_loop_edges_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-for-loop-edges"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/for_loop_edges", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
for_loop_diagnostics_cover_iterables_captures_and_scope :: proc(t: ^testing.T) {
|
|
text := `bad_iterable func() void {
|
|
for 1 |item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_array_pointer_capture func() void {
|
|
for [1] |@item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_range_pointer_capture func() void {
|
|
for 0..1 |@item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_range_index_capture func() void {
|
|
for 0..1 |item, index| {
|
|
_ = item
|
|
_ = index
|
|
}
|
|
}
|
|
bad_duplicate_capture func() void {
|
|
for [1] |item, item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_capture_redeclaration func() void {
|
|
for [1] |item| {
|
|
item i32 = 2
|
|
_ = item
|
|
}
|
|
}
|
|
bad_capture_assignment func() void {
|
|
for [1] |item| {
|
|
item = 2
|
|
}
|
|
}
|
|
bad_immutable_pointer_capture func() void {
|
|
items :: [1]
|
|
for (&items) |@item| {
|
|
item^ = 2
|
|
}
|
|
}
|
|
bad_scope func() void {
|
|
for [1] |item| {
|
|
_ = item
|
|
}
|
|
_ = item
|
|
}
|
|
bad_integer_bounds func() void {
|
|
start i32 = 0
|
|
end u32 = 1
|
|
for start..end |item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_float_bounds func() void {
|
|
for 0.0..1.0 |item| {
|
|
_ = item
|
|
}
|
|
}
|
|
main func() void {
|
|
bad_iterable()
|
|
bad_array_pointer_capture()
|
|
bad_range_pointer_capture()
|
|
bad_range_index_capture()
|
|
bad_duplicate_capture()
|
|
bad_capture_redeclaration()
|
|
bad_capture_assignment()
|
|
bad_immutable_pointer_capture()
|
|
bad_scope()
|
|
bad_integer_bounds()
|
|
bad_float_bounds()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
unsupported := false
|
|
array_pointer := false
|
|
range_pointer := false
|
|
range_index := false
|
|
duplicate_capture := false
|
|
redeclaration := false
|
|
immutable := false
|
|
immutable_pointer := false
|
|
scope := false
|
|
integer_bounds := 0
|
|
for diagnostic in diagnostics.items {
|
|
unsupported = unsupported || strings.contains(diagnostic.message, "for-loop iterable must be a range")
|
|
array_pointer = array_pointer || strings.contains(diagnostic.message, "pointer capture over an array requires")
|
|
range_pointer = range_pointer || strings.contains(diagnostic.message, "range loops do not support pointer captures")
|
|
range_index = range_index || strings.contains(diagnostic.message, "range loops do not support index captures")
|
|
duplicate_capture = duplicate_capture || strings.contains(diagnostic.message, "for-loop captures must have distinct names")
|
|
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'")
|
|
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'item'")
|
|
immutable_pointer = immutable_pointer || strings.contains(diagnostic.message, "assignment target is not writable")
|
|
scope = scope || strings.contains(diagnostic.message, "unknown symbol 'item'")
|
|
integer_bounds += 1 if strings.contains(diagnostic.message, "range bounds must be compatible concrete integers") else 0
|
|
}
|
|
testing.expect(t, unsupported)
|
|
testing.expect(t, array_pointer)
|
|
testing.expect(t, range_pointer)
|
|
testing.expect(t, range_index)
|
|
testing.expect(t, duplicate_capture)
|
|
testing.expect(t, redeclaration)
|
|
testing.expect(t, immutable)
|
|
testing.expect(t, immutable_pointer)
|
|
testing.expect(t, scope)
|
|
testing.expect_value(t, integer_bounds, 2)
|
|
}
|
|
|
|
@(test)
|
|
for_pointer_capture_respects_pointer_and_array_mutability :: proc(t: ^testing.T) {
|
|
text := `readonly func() void {
|
|
values [1]mut i32 = [1]
|
|
items @[1]mut i32 = &values
|
|
items[0] = 7
|
|
for items |@item| {
|
|
item^ = 7
|
|
}
|
|
}
|
|
writable func() void {
|
|
values [1]mut i32 = [1]
|
|
items @mut [1]mut i32 = &values
|
|
items[0] = 7
|
|
for items |@item| {
|
|
item^ = 7
|
|
}
|
|
}
|
|
main func() void {
|
|
readonly()
|
|
writable()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
readonly_errors := 0
|
|
for diagnostic in diagnostics.items {
|
|
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
|
|
}
|
|
testing.expect_value(t, readonly_errors, 2)
|
|
}
|
|
|
|
@(test)
|
|
pointer_field_passthrough_respects_pointee_mutability :: proc(t: ^testing.T) {
|
|
text := `Point :: struct {
|
|
x i32
|
|
}
|
|
readonly func(point @Point) i32 {
|
|
return point.x
|
|
}
|
|
bad_write func(point @Point) void {
|
|
point.x = 7
|
|
}
|
|
writable func(point @mut Point) void {
|
|
point.x += 1
|
|
}
|
|
main func() i32 {
|
|
point Point = Point { x = 41 }
|
|
writable(&point)
|
|
bad_write(&point)
|
|
return readonly(&point)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
readonly_errors := 0
|
|
for diagnostic in diagnostics.items {
|
|
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
|
|
}
|
|
testing.expect_value(t, readonly_errors, 1)
|
|
}
|
|
|
|
@(test)
|
|
equal_range_returns_infer_a_usable_result_type :: proc(t: ^testing.T) {
|
|
text := `choose func(first bool) range {
|
|
if first {
|
|
return 0..1
|
|
}
|
|
return 2..3
|
|
}
|
|
main func() i32 {
|
|
total i32 = 0
|
|
for choose(false) |value| {
|
|
total = total + value
|
|
}
|
|
return total
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
found := false
|
|
for function in hir_module.functions {
|
|
if symbol.resolve(&symbols, function.name) == "choose" {
|
|
found = true
|
|
testing.expect(t, types.is_range(function.result, &hir_module.types))
|
|
testing.expect_value(t, types.child_type(function.result, &hir_module.types), types.I8)
|
|
}
|
|
}
|
|
testing.expect(t, found)
|
|
testing.expect(t, strings.contains(llvm_text, "extractvalue"))
|
|
}
|
|
|
|
@(test)
|
|
for_loop_lowering_evaluates_once_and_avoids_index_bounds_checks :: proc(t: ^testing.T) {
|
|
text := `make_range func() range {
|
|
return 0..2
|
|
}
|
|
make_array func() [2]i32 {
|
|
return [1, 2]
|
|
}
|
|
main func() i32 {
|
|
total i32 = 0
|
|
for make_range() |value| {
|
|
total = total + value
|
|
}
|
|
for make_array() |value| {
|
|
total = total + value
|
|
}
|
|
return total
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
call_count := 0
|
|
extract_count := 0
|
|
select_count := 0
|
|
pointer_add_count := 0
|
|
index_address_count := 0
|
|
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
|
|
testing.expect(t, first_loop_label >= 0)
|
|
for function in ir_module.functions {
|
|
if !function.is_main {
|
|
continue
|
|
}
|
|
for instruction, instruction_index in function.instructions {
|
|
#partial switch instruction.op {
|
|
case .Call: call_count += 1
|
|
case .Extract: extract_count += 1
|
|
case .Select: select_count += 1
|
|
case .Pointer_Add: pointer_add_count += 1
|
|
case .Index_Address: index_address_count += 1
|
|
case .Alloca:
|
|
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
|
|
offset := find_substring_offset(llvm_text, needle)
|
|
testing.expect(t, offset >= 0 && offset < first_loop_label)
|
|
case:
|
|
}
|
|
}
|
|
}
|
|
testing.expect_value(t, call_count, 2)
|
|
testing.expect_value(t, extract_count, 3)
|
|
testing.expect_value(t, select_count, 2)
|
|
testing.expect(t, pointer_add_count >= 1)
|
|
testing.expect_value(t, index_address_count, 0)
|
|
testing.expect(t, !strings.contains(llvm_text, "index_ok"))
|
|
}
|
|
|
|
@(test)
|
|
lexer_emits_compound_assignment_and_slash_tokens :: proc(t: ^testing.T) {
|
|
source_file := source.Source{path="test.bro", text="+= -= *= /= / *"}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, stream.items[0].kind, token.Kind.Plus_Equal)
|
|
testing.expect_value(t, stream.items[1].kind, token.Kind.Minus_Equal)
|
|
testing.expect_value(t, stream.items[2].kind, token.Kind.Star_Equal)
|
|
testing.expect_value(t, stream.items[3].kind, token.Kind.Slash_Equal)
|
|
testing.expect_value(t, stream.items[4].kind, token.Kind.Slash)
|
|
testing.expect_value(t, stream.items[5].kind, token.Kind.Star)
|
|
}
|
|
|
|
@(test)
|
|
binary_operators_respect_multiplicative_precedence :: proc(t: ^testing.T) {
|
|
source_file := source.Source{path="test.bro", text="value :: 1 + 2 * 3\nmain func() void {}\n"}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
root := module.exprs[module.globals[0].expr]
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, root.kind, ast.Expr_Kind.Add)
|
|
testing.expect_value(t, module.exprs[root.left].integer, u64(1))
|
|
testing.expect_value(t, module.exprs[root.right].kind, ast.Expr_Kind.Mul)
|
|
}
|
|
|
|
@(test)
|
|
division_parses_left_associatively :: proc(t: ^testing.T) {
|
|
source_file := source.Source{path="test.bro", text="value :: 8 / 4 / 2\nmain func() void {}\n"}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
root := module.exprs[module.globals[0].expr]
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, root.kind, ast.Expr_Kind.Div)
|
|
testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Div)
|
|
testing.expect_value(t, module.exprs[root.right].integer, u64(2))
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_preserves_operation_and_rhs :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
x i32 = 0
|
|
x += 5
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
body := module.functions[0].body
|
|
statement := module.statements[body[1]]
|
|
testing.expect_value(t, statement.kind, ast.Stmt_Kind.Assignment)
|
|
testing.expect_value(t, statement.assignment_op, ast.Assignment_Op.Add)
|
|
testing.expect(t, statement.target != ast.INVALID_EXPR)
|
|
testing.expect_value(t, module.exprs[statement.target].kind, ast.Expr_Kind.Name)
|
|
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Integer)
|
|
testing.expect_value(t, module.exprs[statement.expr].integer, u64(5))
|
|
}
|
|
|
|
@(test)
|
|
undefined_inferred_local_lowers_to_fill :: proc(t: ^testing.T) {
|
|
text := `choose func(flag bool) i32 {
|
|
value int = undefined
|
|
if flag {
|
|
value = 42
|
|
} else {
|
|
value = -2
|
|
}
|
|
return value
|
|
}
|
|
main func() i32 {
|
|
return choose(true)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
choose_symbol := symbol.intern(&symbols, "choose")
|
|
value_symbol := symbol.intern(&symbols, "value")
|
|
found_value_i32 := false
|
|
fill_count := 0
|
|
for function, function_index in hir_module.functions {
|
|
if function.name != choose_symbol {
|
|
continue
|
|
}
|
|
for local in function.locals {
|
|
found_value_i32 = found_value_i32 || local.name == value_symbol && local.type == types.I32
|
|
}
|
|
for instruction in ir_module.functions[function_index].instructions {
|
|
fill_count += 1 if instruction.op == .Fill else 0
|
|
}
|
|
}
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found_value_i32)
|
|
testing.expect_value(t, fill_count, 1)
|
|
testing.expect(t, strings.contains(llvm_text, "declare void @llvm.memset.p0.i64"))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memset.p0.i64"))
|
|
testing.expect(t, strings.contains(llvm_text, "i8 -86"))
|
|
}
|
|
|
|
@(test)
|
|
local_int_inference_widens_from_assignments :: proc(t: ^testing.T) {
|
|
text := `wide func() int {
|
|
value int = 1
|
|
value = 1000
|
|
return value
|
|
}
|
|
main func() void {
|
|
_ = wide()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
wide_symbol := symbol.intern(&symbols, "wide")
|
|
value_symbol := symbol.intern(&symbols, "value")
|
|
found_value_i16 := false
|
|
found_result_i16 := false
|
|
for function in hir_module.functions {
|
|
if function.name != wide_symbol {
|
|
continue
|
|
}
|
|
found_result_i16 = function.result == types.I16
|
|
for local in function.locals {
|
|
found_value_i16 = found_value_i16 || local.name == value_symbol && local.type == types.I16
|
|
}
|
|
}
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found_result_i16)
|
|
testing.expect(t, found_value_i16)
|
|
}
|
|
|
|
float_local_type :: proc(hir_module: ^hir.Module, symbols: ^symbol.Table, function_name, local_name: string) -> (types.Type, bool) {
|
|
fn_symbol := symbol.intern(symbols, function_name)
|
|
loc_symbol := symbol.intern(symbols, local_name)
|
|
for function in hir_module.functions {
|
|
if function.name != fn_symbol {
|
|
continue
|
|
}
|
|
for local in function.locals {
|
|
if local.name == loc_symbol {
|
|
return local.type, true
|
|
}
|
|
}
|
|
}
|
|
return types.INVALID, false
|
|
}
|
|
|
|
float_result_type :: proc(hir_module: ^hir.Module, symbols: ^symbol.Table, function_name: string) -> (types.Type, bool) {
|
|
fn_symbol := symbol.intern(symbols, function_name)
|
|
for function in hir_module.functions {
|
|
if function.name == fn_symbol {
|
|
return function.result, true
|
|
}
|
|
}
|
|
return types.INVALID, false
|
|
}
|
|
|
|
@(test)
|
|
float_constraint_resolves_to_f64 :: proc(t: ^testing.T) {
|
|
text := `make func() float {
|
|
pi float = 3.14
|
|
return pi
|
|
}
|
|
main func() void {
|
|
_ = make()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
pi_type, found := float_local_type(&hir_module, &symbols, "make", "pi")
|
|
result_type, _ := float_result_type(&hir_module, &symbols, "make")
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found)
|
|
testing.expect_value(t, pi_type, types.F64)
|
|
testing.expect_value(t, result_type, types.F64)
|
|
}
|
|
|
|
@(test)
|
|
float_constraint_accepts_integer_literal :: proc(t: ^testing.T) {
|
|
text := `make func() float {
|
|
pi float = 3
|
|
return pi
|
|
}
|
|
main func() void {
|
|
_ = make()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
pi_type, found := float_local_type(&hir_module, &symbols, "make", "pi")
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found)
|
|
testing.expect_value(t, pi_type, types.F64)
|
|
}
|
|
|
|
@(test)
|
|
float_constraint_result_resolves_to_f64 :: proc(t: ^testing.T) {
|
|
text := `make func() float {
|
|
return 3.0
|
|
}
|
|
main func() void {
|
|
_ = make()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
result_type, found := float_result_type(&hir_module, &symbols, "make")
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found)
|
|
testing.expect_value(t, result_type, types.F64)
|
|
}
|
|
|
|
@(test)
|
|
float_constraint_widens_f32_to_f64 :: proc(t: ^testing.T) {
|
|
text := `wide func(a f32, b f64) float {
|
|
x float = a
|
|
x = b
|
|
return x
|
|
}
|
|
main func() void {
|
|
_ = wide(1.0, 2.0)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
x_type, found := float_local_type(&hir_module, &symbols, "wide", "x")
|
|
result_type, _ := float_result_type(&hir_module, &symbols, "wide")
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found)
|
|
testing.expect_value(t, x_type, types.F64)
|
|
testing.expect_value(t, result_type, types.F64)
|
|
}
|
|
|
|
@(test)
|
|
int_constraint_rejects_float_initializer :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
x int = 1.0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_constraint_error := false
|
|
for diagnostic in diagnostics.items {
|
|
found_constraint_error = found_constraint_error ||
|
|
strings.contains(diagnostic.message, "could not resolve the 'int' constraint for local 'x'")
|
|
}
|
|
|
|
testing.expect(t, found_constraint_error)
|
|
}
|
|
|
|
@(test)
|
|
float_constraint_rejects_runtime_integer :: proc(t: ^testing.T) {
|
|
text := `take func(n i32) void {
|
|
x float = n
|
|
}
|
|
main func() void {
|
|
take(7)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_convert_error := false
|
|
for diagnostic in diagnostics.items {
|
|
found_convert_error = found_convert_error ||
|
|
strings.contains(diagnostic.message, "cannot implicitly convert i32 to f64")
|
|
}
|
|
|
|
testing.expect(t, found_convert_error)
|
|
}
|
|
|
|
@(test)
|
|
range_constraint_local_resolves_to_inferred_range :: proc(t: ^testing.T) {
|
|
text := `make func() range {
|
|
r range :: 0..10
|
|
return r
|
|
}
|
|
main func() void {
|
|
_ = make()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
r_type, found := float_local_type(&hir_module, &symbols, "make", "r")
|
|
result_type, _ := float_result_type(&hir_module, &symbols, "make")
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found)
|
|
testing.expect(t, types.is_range(r_type, &hir_module.types))
|
|
testing.expect_value(t, types.child_type(r_type, &hir_module.types), types.I8)
|
|
testing.expect(t, types.is_range(result_type, &hir_module.types))
|
|
}
|
|
|
|
@(test)
|
|
range_constraint_param_and_result_monomorphize :: proc(t: ^testing.T) {
|
|
text := `pass func(r range) range {
|
|
return r
|
|
}
|
|
main func() void {
|
|
once :: 0..5
|
|
for pass(once) |v| {
|
|
_ = v
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
result_type, found := float_result_type(&hir_module, &symbols, "pass")
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found)
|
|
testing.expect(t, types.is_range(result_type, &hir_module.types))
|
|
}
|
|
|
|
@(test)
|
|
int_param_rejects_float_argument :: proc(t: ^testing.T) {
|
|
text := `take func(x int) int {
|
|
return x
|
|
}
|
|
main func() void {
|
|
_ = take(1.5)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_reject := false
|
|
for diagnostic in diagnostics.items {
|
|
found_reject = found_reject ||
|
|
strings.contains(diagnostic.message, "cannot pass f64 to 'int' parameter 'x'")
|
|
}
|
|
|
|
testing.expect(t, found_reject)
|
|
}
|
|
|
|
@(test)
|
|
float_param_accepts_integer_literal_argument :: proc(t: ^testing.T) {
|
|
text := `take func(x float) float {
|
|
return x
|
|
}
|
|
main func() void {
|
|
y f64 = take(3)
|
|
_ = y
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
result_type, found := float_result_type(&hir_module, &symbols, "take")
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found)
|
|
testing.expect_value(t, result_type, types.F64)
|
|
}
|
|
|
|
@(test)
|
|
undefined_accepts_concrete_runtime_annotations :: proc(t: ^testing.T) {
|
|
text := `Point :: struct {
|
|
x i32
|
|
y i32
|
|
}
|
|
main func() void {
|
|
point Point = undefined
|
|
pointer @i32 = undefined
|
|
maybe ?i32 = undefined
|
|
_ = point
|
|
_ = pointer
|
|
_ = maybe
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
main_symbol := symbol.intern(&symbols, "main")
|
|
fill_count := 0
|
|
for function, function_index in hir_module.functions {
|
|
if function.name != main_symbol {
|
|
continue
|
|
}
|
|
for instruction in ir_module.functions[function_index].instructions {
|
|
fill_count += 1 if instruction.op == .Fill else 0
|
|
}
|
|
}
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, fill_count, 3)
|
|
}
|
|
|
|
@(test)
|
|
undefined_rejects_non_declaration_uses_and_unresolved_inference :: proc(t: ^testing.T) {
|
|
text := `global :: undefined
|
|
main func() void {
|
|
immutable :: undefined
|
|
typed_immutable int :: undefined
|
|
unresolved int = undefined
|
|
existing i32 = 1
|
|
existing = undefined
|
|
mismatch int = undefined
|
|
mismatch = 1
|
|
mismatch = 1.0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
immutable_count := 0
|
|
found_unresolved := false
|
|
found_assignment := false
|
|
found_incompatible := false
|
|
for diagnostic in diagnostics.items {
|
|
immutable_count += 1 if strings.contains(diagnostic.message, "'undefined' requires a mutable local declaration") else 0
|
|
found_unresolved = found_unresolved || strings.contains(diagnostic.message, "could not infer a concrete type for local 'unresolved'")
|
|
found_assignment = found_assignment || strings.contains(diagnostic.message, "'undefined' is only valid as a mutable local declaration initializer")
|
|
found_incompatible = found_incompatible || strings.contains(diagnostic.message, "cannot implicitly convert f64 to i8")
|
|
}
|
|
|
|
testing.expect(t, immutable_count >= 2)
|
|
testing.expect(t, found_unresolved)
|
|
testing.expect(t, found_assignment)
|
|
testing.expect(t, found_incompatible)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) {
|
|
// A compound assignment to an indexed lvalue must compute the element address
|
|
// once and reuse it for the load and the store, rather than re-lowering the
|
|
// lvalue (which would re-evaluate any side-effecting index subexpression).
|
|
text := `bump func() usize {
|
|
return 1
|
|
}
|
|
main func() i32 {
|
|
values [3]mut i32 = [10, 20, 30]
|
|
values[bump()] += 5
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
call_count := 0
|
|
index_address_count := 0
|
|
for function in ir_module.functions {
|
|
if !function.is_main {
|
|
continue
|
|
}
|
|
for instruction in function.instructions {
|
|
#partial switch instruction.op {
|
|
case .Call: call_count += 1
|
|
case .Index_Address: index_address_count += 1
|
|
case:
|
|
}
|
|
}
|
|
}
|
|
// `bump()` is the lvalue's index. The fix shares one address between the load
|
|
// and the store, so the side-effecting index runs exactly once and a single
|
|
// Index_Address is emitted; the buggy double-lowering produced two of each.
|
|
testing.expect_value(t, call_count, 1)
|
|
testing.expect_value(t, index_address_count, 1)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_evaluates_nested_locations_once :: proc(t: ^testing.T) {
|
|
text := `Box :: struct {
|
|
value i32
|
|
}
|
|
row func() usize {
|
|
return 0
|
|
}
|
|
column func() usize {
|
|
return 1
|
|
}
|
|
pointer_for func(value @mut i32) @mut i32 {
|
|
return value
|
|
}
|
|
main func() i32 {
|
|
matrix [2]mut [2]mut i32 = [[1, 2], [3, 4]]
|
|
(matrix[row()])[column()] += 1
|
|
boxes [2]mut Box = [Box { value = 5 }, Box { value = 6 }]
|
|
boxes[row()].value += 1
|
|
value i32 = 7
|
|
pointer_for(&value)^ += 1
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
call_count := 0
|
|
call_names: [4]string
|
|
index_address_count := 0
|
|
field_address_count := 0
|
|
for function in ir_module.functions {
|
|
if !function.is_main {
|
|
continue
|
|
}
|
|
for instruction in function.instructions {
|
|
#partial switch instruction.op {
|
|
case .Call:
|
|
function_id := ir.as_function(instruction.target)
|
|
if call_count < len(call_names) &&
|
|
function_id != ir.INVALID_FUNCTION &&
|
|
int(function_id) < len(hir_module.functions) {
|
|
call_names[call_count] = symbol.resolve(
|
|
&symbols,
|
|
hir_module.functions[function_id].name,
|
|
)
|
|
}
|
|
call_count += 1
|
|
case .Index_Address: index_address_count += 1
|
|
case .Field_Address: field_address_count += 1
|
|
case:
|
|
}
|
|
}
|
|
}
|
|
// row(), column(), the second row(), and pointer_for() each run once. The
|
|
// nested matrix target needs two index addresses; the indexed field needs
|
|
// one index address and one field address.
|
|
testing.expect_value(t, call_count, 4)
|
|
testing.expect_value(t, call_names, [4]string{"row", "column", "row", "pointer_for"})
|
|
testing.expect_value(t, index_address_count, 3)
|
|
testing.expect_value(t, field_address_count, 1)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_supports_pointer_add_only :: proc(t: ^testing.T) {
|
|
valid_text := `main func() i32 {
|
|
values [3]mut i32 = [10, 20, 30]
|
|
pointer *mut i32 = (&values).ptr
|
|
pointer += 1
|
|
offset usize = 1
|
|
pointer += offset
|
|
return pointer^
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=valid_text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
pointer_add_count := 0
|
|
for function in ir_module.functions {
|
|
if !function.is_main {
|
|
continue
|
|
}
|
|
for instruction in function.instructions {
|
|
pointer_add_count += 1 if instruction.op == .Pointer_Add else 0
|
|
}
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, pointer_add_count, 2)
|
|
|
|
invalid_text := `main func() void {
|
|
values [1]mut i32 = [10]
|
|
pointer *mut i32 = (&values).ptr
|
|
pointer -= 1
|
|
}
|
|
`
|
|
invalid_source := source.Source{path="invalid.bro", text=invalid_text}
|
|
invalid_diagnostics := source.init_diagnostics(&invalid_source)
|
|
defer source.destroy_diagnostics(&invalid_diagnostics)
|
|
invalid_symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&invalid_symbols)
|
|
invalid_stream := lexer.lex(&invalid_source, &invalid_diagnostics, &invalid_symbols)
|
|
defer delete(invalid_stream.items)
|
|
invalid_ast := parser.parse(&invalid_stream, &invalid_source, &invalid_diagnostics)
|
|
defer ast.destroy_module(&invalid_ast)
|
|
invalid_hir := checker.check(&invalid_ast, &invalid_diagnostics, &invalid_symbols)
|
|
defer hir.destroy_module(&invalid_hir)
|
|
|
|
found := false
|
|
for diagnostic in invalid_diagnostics.items {
|
|
found = found || strings.contains(
|
|
diagnostic.message,
|
|
"many-item pointers only support '+=' compound assignment",
|
|
)
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_preserves_checked_numeric_operations :: proc(t: ^testing.T) {
|
|
text := `main func() i32 {
|
|
signed i32 = 24
|
|
signed += 6
|
|
signed -= 2
|
|
signed *= 3
|
|
signed = divtrunc!(signed, 4)
|
|
unsigned u32 = 24
|
|
unsigned += 6
|
|
unsigned -= 2
|
|
unsigned *= 3
|
|
unsigned = divtrunc!(unsigned, 4)
|
|
real f64 = 24.0
|
|
real += 6.0
|
|
real -= 2.0
|
|
real *= 3.0
|
|
real /= 4.0
|
|
return signed
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
operation_counts: [hir.Assignment_Op]int
|
|
for statement_id in hir_module.functions[0].body {
|
|
statement := hir_module.statements[statement_id]
|
|
if statement.kind == .Assignment {
|
|
operation_counts[statement.assignment_op] += 1
|
|
}
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, operation_counts[.Add], 3)
|
|
testing.expect_value(t, operation_counts[.Sub], 3)
|
|
testing.expect_value(t, operation_counts[.Mul], 3)
|
|
testing.expect_value(t, operation_counts[.Div], 1)
|
|
|
|
add_count := 0
|
|
sub_count := 0
|
|
mul_count := 0
|
|
div_count := 0
|
|
div_trunc_count := 0
|
|
for instruction in ir_module.functions[0].instructions {
|
|
#partial switch instruction.op {
|
|
case .Add_Checked: add_count += 1
|
|
case .Sub_Checked: sub_count += 1
|
|
case .Mul_Checked: mul_count += 1
|
|
case .Div_Checked: div_count += 1
|
|
case .Div_Trunc_Checked: div_trunc_count += 1
|
|
case:
|
|
}
|
|
}
|
|
testing.expect_value(t, add_count, 3)
|
|
testing.expect_value(t, sub_count, 3)
|
|
testing.expect_value(t, mul_count, 3)
|
|
testing.expect_value(t, div_count, 1)
|
|
testing.expect_value(t, div_trunc_count, 2)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_rejects_narrowing_and_mixed_numeric_families :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
narrow i8 = 1
|
|
wide i32 = 2
|
|
narrow += wide
|
|
signed i32 = 3
|
|
unsigned u32 = 4
|
|
signed += unsigned
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_narrowing := false
|
|
found_mixed_family := false
|
|
for diagnostic in diagnostics.items {
|
|
found_narrowing = found_narrowing ||
|
|
strings.contains(diagnostic.message, "cannot implicitly convert i32 to i8")
|
|
found_mixed_family = found_mixed_family ||
|
|
strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
|
|
}
|
|
testing.expect(t, found_narrowing)
|
|
testing.expect(t, found_mixed_family)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-compound"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/compound_assignment", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 23)
|
|
}
|
|
|
|
@(test)
|
|
binary_arithmetic_rejects_non_numeric_operands :: proc(t: ^testing.T) {
|
|
text := `main func() i32 {
|
|
a i32 = 1
|
|
b u32 = 2
|
|
_ = a + b
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_requires_writable_target :: proc(t: ^testing.T) {
|
|
text := `main func() i32 {
|
|
x :: 5
|
|
x += 1
|
|
return x
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "assignment target is not writable")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) {
|
|
text := `main func() i32 {
|
|
a i32 = 10
|
|
b i32 = 3
|
|
c i32 = a - b
|
|
return divtrunc!(c, b)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i32"))
|
|
testing.expect(t, strings.contains(llvm_text, "sdiv i32"))
|
|
testing.expect(t, strings.contains(llvm_text, "divzero_trap"))
|
|
testing.expect(t, strings.contains(llvm_text, "divovf_trap"))
|
|
}
|
|
|
|
@(test)
|
|
integer_slash_is_rejected_and_float_slash_remains_available :: proc(t: ^testing.T) {
|
|
Case :: struct {text, want: string}
|
|
invalid := []Case{
|
|
{text=`main func() void {
|
|
a i32 = 4
|
|
b i32 = 2
|
|
_ = a / b
|
|
}`, want="integer '/' is not allowed"},
|
|
{text=`main func() void {
|
|
a u32 = 4
|
|
b u32 = 2
|
|
_ = a / b
|
|
}`, want="integer '/' is not allowed"},
|
|
{text=`main func() void {
|
|
_ = 4 / 2
|
|
}`, want="integer '/' is not allowed"},
|
|
{text=`main func() void {
|
|
values [4 / 2]u8 = undefined
|
|
_ = &values
|
|
}`, want="integer '/' is not allowed"},
|
|
{text=`half func($value i32) i32 { return value / 2 }
|
|
main func() void { _ = $half(4) }`, want="integer '/' is not allowed"},
|
|
{text=`main func() void {
|
|
value i32 = 8
|
|
value /= 2
|
|
}`, want="assign through an explicit division builtin"},
|
|
}
|
|
for test_case in invalid {
|
|
source_file := source.Source{path="test.bro", text=test_case.text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
symbols := symbol.init_table()
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, test_case.want)
|
|
}
|
|
testing.expect(t, found)
|
|
hir.destroy_module(&hir_module)
|
|
ast.destroy_module(&ast_module)
|
|
delete(stream.items)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
}
|
|
|
|
text := `main func() void {
|
|
value f32 = 5.0 / 2.0
|
|
value /= 2.0
|
|
_ = value
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
division_builtins_diagnose_arity_operands_and_comptime_failures :: proc(t: ^testing.T) {
|
|
text := `bad_arity :: divfloor!(1)
|
|
bad_bool :: rem!(true, false)
|
|
bad_family :: mod!(i32(5), f32(3))
|
|
zero_trunc :: divtrunc!(1, 0)
|
|
zero_floor :: divfloor!(1.0, 0.0)
|
|
zero_exact :: divexact!(1, 0)
|
|
zero_ceil :: divceil!(1.0, 0.0)
|
|
zero_rem :: rem!(1, 0)
|
|
zero_mod :: mod!(1.0, 0.0)
|
|
inexact :: divexact!(5, 3)
|
|
overflow_trunc :: divtrunc!(minval!(i32), -1)
|
|
overflow_floor :: divfloor!(minval!(i32), -1)
|
|
overflow_exact :: divexact!(minval!(i32), -1)
|
|
overflow_ceil :: divceil!(minval!(i32), -1)
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
zero_count := 0
|
|
found_arity, found_operands, found_exact := false, false, false
|
|
overflow_count := 0
|
|
for diagnostic in diagnostics.items {
|
|
found_arity = found_arity || strings.contains(diagnostic.message, "expects 2 arguments")
|
|
found_operands = found_operands || strings.contains(diagnostic.message, "compatible numeric operands")
|
|
found_exact = found_exact || strings.contains(diagnostic.message, "exact division has a remainder")
|
|
if strings.contains(diagnostic.message, "signed integer division overflow") {
|
|
overflow_count += 1
|
|
}
|
|
if strings.contains(diagnostic.message, "division builtin denominator is zero") {
|
|
zero_count += 1
|
|
}
|
|
}
|
|
testing.expect(t, found_arity)
|
|
testing.expect(t, found_operands)
|
|
testing.expect(t, found_exact)
|
|
testing.expect_value(t, overflow_count, 4)
|
|
testing.expect_value(t, zero_count, 6)
|
|
}
|
|
|
|
@(test)
|
|
division_family_compiles_and_runs_for_integer_and_float_scalars :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-division-family"
|
|
main_path := "/tmp/brolang-test-division-family/main.bro"
|
|
output := "/tmp/brolang-test-division-family-output"
|
|
text := `COUNT :: divexact!(8, 2)
|
|
items [divceil!(10, 3)]u8 :: [0, 0, 0, 0]
|
|
OPEN :: 5
|
|
open_ceil i32 :: divceil!(OPEN, 3)
|
|
|
|
check_i32 func(a, b, qt, qf, qc, r, m i32) bool {
|
|
return divtrunc!(a, b) == qt and divfloor!(a, b) == qf and
|
|
divceil!(a, b) == qc and rem!(a, b) == r and mod!(a, b) == m
|
|
}
|
|
|
|
check_f32 func(a, b, qt, qf, qc, r, m f32) bool {
|
|
return divtrunc!(a, b) == qt and divfloor!(a, b) == qf and
|
|
divceil!(a, b) == qc and rem!(a, b) == r and mod!(a, b) == m
|
|
}
|
|
|
|
check_f64 func(a, b, qt, qf, qc, r, m f64) bool {
|
|
return divtrunc!(a, b) == qt and divfloor!(a, b) == qf and
|
|
divceil!(a, b) == qc and rem!(a, b) == r and mod!(a, b) == m
|
|
}
|
|
|
|
edge_rem func(a, b i32) i32 { return rem!(a, b) }
|
|
edge_mod func(a, b i32) i32 { return mod!(a, b) }
|
|
|
|
main func() i32 {
|
|
if COUNT != 4 or items.len != 4 or open_ceil != 2 { return 1 }
|
|
if !check_i32(5, 3, 1, 1, 2, 2, 2) { return 2 }
|
|
if !check_i32(5, -3, -1, -2, -1, 2, -1) { return 3 }
|
|
if !check_i32(-5, 3, -1, -2, -1, -2, 1) { return 4 }
|
|
if !check_i32(-5, -3, 1, 1, 2, -2, -2) { return 5 }
|
|
if divtrunc!(u32(5), u32(3)) != 1 or divfloor!(u32(5), u32(3)) != 1 or
|
|
divceil!(u32(5), u32(3)) != 2 or rem!(u32(5), u32(3)) != 2 or mod!(u32(5), u32(3)) != 2 { return 6 }
|
|
if divexact!(i32(6), i32(3)) != 2 or divexact!(u32(6), u32(3)) != 2 { return 7 }
|
|
if !check_f32(f32(5.0), f32(3.0), f32(1.0), f32(1.0), f32(2.0), f32(2.0), f32(2.0)) or
|
|
!check_f32(f32(5.0), f32(-3.0), f32(-1.0), f32(-2.0), f32(-1.0), f32(2.0), f32(-1.0)) or
|
|
!check_f32(f32(-5.0), f32(3.0), f32(-1.0), f32(-2.0), f32(-1.0), f32(-2.0), f32(1.0)) or
|
|
!check_f32(f32(-5.0), f32(-3.0), f32(1.0), f32(1.0), f32(2.0), f32(-2.0), f32(-2.0)) { return 8 }
|
|
if !check_f64(5.0, 3.0, 1.0, 1.0, 2.0, 2.0, 2.0) or
|
|
!check_f64(5.0, -3.0, -1.0, -2.0, -1.0, 2.0, -1.0) or
|
|
!check_f64(-5.0, 3.0, -1.0, -2.0, -1.0, -2.0, 1.0) or
|
|
!check_f64(-5.0, -3.0, 1.0, 1.0, 2.0, -2.0, -2.0) { return 9 }
|
|
if divexact!(f32(6.0), f32(3.0)) != 2.0 or divexact!(f64(6.0), f64(3.0)) != 2.0 { return 10 }
|
|
if edge_rem(-2147483648, -1) != 0 or edge_mod(-2147483648, -1) != 0 { return 11 }
|
|
return 0
|
|
}
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
status := compiler_core.compile_package(directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
division_builtins_trap_for_runtime_zero_overflow_and_inexact_results :: proc(t: ^testing.T) {
|
|
Case :: struct {
|
|
name: string,
|
|
type_name: string,
|
|
left: string,
|
|
right: string,
|
|
}
|
|
cases := [?]Case{
|
|
{name="divtrunc", type_name="i32", left="1", right="0"},
|
|
{name="divfloor", type_name="f32", left="f32(1.0)", right="f32(0.0)"},
|
|
{name="divexact", type_name="f64", left="1.0", right="0.0"},
|
|
{name="divceil", type_name="i32", left="1", right="0"},
|
|
{name="rem", type_name="f32", left="f32(1.0)", right="f32(0.0)"},
|
|
{name="mod", type_name="f64", left="1.0", right="0.0"},
|
|
{name="divexact", type_name="i32", left="5", right="3"},
|
|
{name="divtrunc", type_name="i32", left="-2147483648", right="-1"},
|
|
{name="divfloor", type_name="i32", left="-2147483648", right="-1"},
|
|
{name="divexact", type_name="i32", left="-2147483648", right="-1"},
|
|
{name="divceil", type_name="i32", left="-2147483648", right="-1"},
|
|
}
|
|
for test_case, index in cases {
|
|
directory := fmt.aprintf("/tmp/brolang-test-division-trap-%d", index)
|
|
main_path := fmt.aprintf("%s/main.bro", directory)
|
|
output := fmt.aprintf("/tmp/brolang-test-division-trap-output-%d", index)
|
|
text := fmt.aprintf(
|
|
"invoke func(a, b %s) %s {{ return %s!(a, b) }}\nmain func() void {{ _ = invoke(%s, %s) }}\n",
|
|
test_case.type_name, test_case.type_name, test_case.name, test_case.left, test_case.right,
|
|
)
|
|
_ = os2.remove_all(directory)
|
|
_ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
status := compiler_core.compile_package(directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
_ = os.remove(output)
|
|
_ = os2.remove_all(directory)
|
|
delete(text)
|
|
delete(output)
|
|
delete(main_path)
|
|
delete(directory)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
qualified_division_builtin_names_resolve_as_package_functions :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-qualified-division"
|
|
math_directory := "/tmp/brolang-test-qualified-division/math"
|
|
app_directory := "/tmp/brolang-test-qualified-division/app"
|
|
math_path := "/tmp/brolang-test-qualified-division/math/math.bro"
|
|
main_path := "/tmp/brolang-test-qualified-division/app/main.bro"
|
|
output := "/tmp/brolang-test-qualified-division-output"
|
|
math_text := `divfloor func(a, b i32) i32 { return a + b }
|
|
`
|
|
main_text := `math :: import "../math"
|
|
main func() i32 { return math.divfloor(20, 22) }
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.make_directory(math_directory) == nil)
|
|
testing.expect(t, os.make_directory(app_directory) == nil)
|
|
testing.expect(t, os.write_entire_file(math_path, transmute([]byte)math_text))
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)main_text))
|
|
status := compiler_core.compile_package(app_directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
division_builtins_emit_guards_rounding_and_single_integer_divisions :: proc(t: ^testing.T) {
|
|
text := `floor_i32 func(a, b i32) i32 { return divfloor!(a, b) }
|
|
ceil_i32 func(a, b i32) i32 { return divceil!(a, b) }
|
|
exact_i32 func(a, b i32) i32 { return divexact!(a, b) }
|
|
floor_u32 func(a, b u32) u32 { return divfloor!(a, b) }
|
|
rem_i16 func(a, b i16) i16 { return rem!(a, b) }
|
|
mod_i16 func(a, b i16) i16 { return mod!(a, b) }
|
|
floor_f32 func(a, b f32) f32 { return divfloor!(a, b) }
|
|
ceil_f64 func(a, b f64) f64 { return divceil!(a, b) }
|
|
exact_f32 func(a, b f32) f32 { return divexact!(a, b) }
|
|
rem_f64 func(a, b f64) f64 { return rem!(a, b) }
|
|
mod_f32 func(a, b f32) f32 { return mod!(a, b) }
|
|
main func() void {
|
|
_ = floor_i32(5, 3)
|
|
_ = ceil_i32(5, 3)
|
|
_ = exact_i32(6, 3)
|
|
_ = floor_u32(5, 3)
|
|
_ = rem_i16(5, 3)
|
|
_ = mod_i16(5, 3)
|
|
_ = floor_f32(f32(5.0), f32(3.0))
|
|
_ = ceil_f64(5.0, 3.0)
|
|
_ = exact_f32(f32(6.0), f32(3.0))
|
|
_ = rem_f64(5.0, 3.0)
|
|
_ = mod_f32(f32(5.0), f32(3.0))
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, strings.count(llvm_text, "sdiv i32"), 3)
|
|
testing.expect(t, !strings.contains(llvm_text, "srem i32"))
|
|
testing.expect(t, strings.contains(llvm_text, "udiv i32"))
|
|
testing.expect(t, strings.contains(llvm_text, "srem i16"))
|
|
testing.expect(t, strings.contains(llvm_text, "remspecial"))
|
|
testing.expect(t, strings.contains(llvm_text, "divzero_trap"))
|
|
testing.expect(t, strings.contains(llvm_text, "divovf_trap"))
|
|
testing.expect(t, strings.contains(llvm_text, "call float @llvm.floor.f32"))
|
|
testing.expect(t, strings.contains(llvm_text, "call double @llvm.ceil.f64"))
|
|
testing.expect(t, strings.contains(llvm_text, "call float @llvm.trunc.f32"))
|
|
testing.expect(t, strings.contains(llvm_text, "frem double"))
|
|
}
|
|
|
|
@(test)
|
|
distinct_types_preserve_nominal_identity_and_backing_representation :: proc(t: ^testing.T) {
|
|
text := `Point :: struct {
|
|
x i32
|
|
y i32
|
|
}
|
|
UserID :: distinct u32
|
|
OtherID :: distinct u32
|
|
PointID :: distinct Point
|
|
Bytes :: distinct [2]u8
|
|
WrappedID :: distinct UserID
|
|
static_id UserID :: UserID(42)
|
|
take func(value UserID) UserID {
|
|
return value
|
|
}
|
|
main func() i32 {
|
|
id UserID :: UserID(7)
|
|
copy UserID = take(id)
|
|
maybe ?UserID = copy
|
|
pointer @UserID = ©
|
|
point PointID :: PointID(Point { x = 1, y = 2 })
|
|
bytes Bytes :: Bytes([3, 4])
|
|
wrapped WrappedID :: WrappedID(id)
|
|
_ = maybe
|
|
_ = pointer
|
|
_ = point
|
|
_ = bytes
|
|
_ = wrapped
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
user_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "UserID")))
|
|
other_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "OtherID")))
|
|
point_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "PointID")))
|
|
point := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Point")))
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, user_id != other_id)
|
|
testing.expect(t, user_id != types.U32)
|
|
testing.expect(t, types.is_distinct(user_id, &ast_module.type_store))
|
|
testing.expect_value(t, types.runtime_representation(user_id, &ast_module.type_store), types.U32)
|
|
testing.expect_value(t, types.runtime_representation(point_id, &ast_module.type_store), point)
|
|
testing.expect_value(t, types.size(user_id, &ast_module.type_store), types.size(types.U32, &ast_module.type_store))
|
|
testing.expect(t, hir_module.globals[0].is_static)
|
|
testing.expect_value(t, hir_module.globals[0].static_value, i64(42))
|
|
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i32 42"))
|
|
testing.expect(t, strings.contains(llvm_text, "select i1 true, i32"))
|
|
|
|
retype_count := 0
|
|
for function in ir_module.functions {
|
|
for instruction in function.instructions {
|
|
retype_count += 1 if instruction.op == .Retype else 0
|
|
}
|
|
}
|
|
testing.expect_value(t, retype_count, 4)
|
|
}
|
|
|
|
@(test)
|
|
distinct_types_reject_implicit_conversions_operators_and_invalid_backings :: proc(t: ^testing.T) {
|
|
text := `Opaque :: opaque
|
|
UserID :: distinct u32
|
|
OtherID :: distinct u32
|
|
BadInt :: distinct int
|
|
BadVoid :: distinct void
|
|
BadFunction :: distinct c_func() void
|
|
BadOpaque :: distinct Opaque
|
|
foreign c_func(value UserID) void
|
|
foreign_pointer c_func(value @UserID) void
|
|
main func() void {
|
|
raw u32 = 1
|
|
id UserID = raw
|
|
backing u32 = UserID(2)
|
|
other OtherID = UserID(3)
|
|
narrow u8 = 4
|
|
_ = UserID(narrow)
|
|
_ = UserID()
|
|
_ = UserID(1, 2)
|
|
left UserID :: UserID(5)
|
|
right UserID :: UserID(6)
|
|
_ = left + right
|
|
_ = left == right
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
invalid_backing_count := 0
|
|
implicit_conversion_count := 0
|
|
found_exact := false
|
|
found_arity := false
|
|
found_arithmetic := false
|
|
found_comparison := false
|
|
foreign_signature_count := 0
|
|
for diagnostic in diagnostics.items {
|
|
invalid_backing_count += 1 if strings.contains(diagnostic.message, "requires a concrete runtime backing type") else 0
|
|
implicit_conversion_count += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0
|
|
found_exact = found_exact || strings.contains(diagnostic.message, "requires an exact u32 value, got u8")
|
|
found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 argument")
|
|
found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
|
|
found_comparison = found_comparison || strings.contains(diagnostic.message, "comparison requires compatible numeric operands")
|
|
foreign_signature_count += 1 if strings.contains(diagnostic.message, "requires concrete parameter types") else 0
|
|
}
|
|
testing.expect_value(t, invalid_backing_count, 4)
|
|
testing.expect(t, implicit_conversion_count >= 3)
|
|
testing.expect(t, found_exact)
|
|
testing.expect(t, found_arity)
|
|
testing.expect(t, found_arithmetic)
|
|
testing.expect(t, found_comparison)
|
|
testing.expect_value(t, foreign_signature_count, 2)
|
|
}
|
|
|
|
@(test)
|
|
type_and_function_names_cannot_shadow :: proc(t: ^testing.T) {
|
|
text := `Value :: distinct u32
|
|
Value func(value i32) i32 {
|
|
return value
|
|
}
|
|
main func() i32 {
|
|
return Value(42)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "function 'Value' shadows visible type")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
distinct_types_compile_and_run_across_packages :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-distinct-types"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/distinct_types", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
native_enums_preserve_identity_members_and_integer_representation :: proc(t: ^testing.T) {
|
|
text := `Animal :: enum {
|
|
dog
|
|
cat
|
|
bird
|
|
}
|
|
Nat :: enum(u16) {
|
|
one = 1
|
|
two
|
|
five = 5
|
|
}
|
|
global Animal :: Animal.dog
|
|
take func(value Animal) Animal {
|
|
return value
|
|
}
|
|
identity c_func(value Nat) Nat {
|
|
return value
|
|
}
|
|
variadic c_func(marker c_int, ...) c_int
|
|
main func() i32 {
|
|
value Animal = .cat
|
|
values [2]Animal :: [.dog, Animal.bird]
|
|
number Nat = identity(.two)
|
|
ordinal c_int :: c_int(number)
|
|
_ = variadic(0, number)
|
|
if take(value) == Animal.cat and values[0] != values[1] and number == Nat.two and ordinal == 2 {
|
|
return 0
|
|
}
|
|
return 1
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
animal := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Animal")))
|
|
nat := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Nat")))
|
|
animal_node, animal_ok := types.node(&ast_module.type_store, animal)
|
|
nat_node, nat_ok := types.node(&ast_module.type_store, nat)
|
|
animal_members := types.enum_members_for(&ast_module.type_store, animal)
|
|
nat_members := types.enum_members_for(&ast_module.type_store, nat)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, animal_ok && nat_ok)
|
|
testing.expect(t, types.is_enum(animal, &ast_module.type_store))
|
|
testing.expect_value(t, animal_node.child, types.U16)
|
|
testing.expect(t, !animal_node.explicit_backing)
|
|
testing.expect_value(t, nat_node.child, types.U16)
|
|
testing.expect(t, nat_node.explicit_backing)
|
|
testing.expect_value(t, len(animal_members), 3)
|
|
testing.expect_value(t, animal_members[0].value, i128(1))
|
|
testing.expect_value(t, animal_members[2].value, i128(3))
|
|
testing.expect_value(t, nat_members[0].value, i128(1))
|
|
testing.expect_value(t, nat_members[1].value, i128(2))
|
|
testing.expect_value(t, nat_members[2].value, i128(5))
|
|
testing.expect(t, strings.contains(llvm_text, "zext i16"))
|
|
testing.expect_value(t, types.runtime_representation(animal, &ast_module.type_store), types.U16)
|
|
testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(2))
|
|
testing.expect(t, hir_module.globals[0].is_static)
|
|
testing.expect_value(t, hir_module.globals[0].static_value, i64(1))
|
|
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i16 1"))
|
|
found_promotion := false
|
|
for function in ir_module.functions {
|
|
for instruction in function.instructions {
|
|
found_promotion = found_promotion || instruction.op == .C_Vararg_Promote
|
|
}
|
|
}
|
|
testing.expect(t, found_promotion)
|
|
}
|
|
|
|
@(test)
|
|
keywords_are_valid_enum_members_and_tagged_union_variants :: proc(t: ^testing.T) {
|
|
testing.expect(t, token.is_keyword(.Keyword_Func))
|
|
testing.expect(t, token.is_keyword(.Keyword_Hide))
|
|
testing.expect(t, token.is_keyword(.Keyword_C_Longdouble))
|
|
testing.expect(t, !token.is_keyword(.Identifier))
|
|
testing.expect(t, !token.is_keyword(.Underscore))
|
|
|
|
text := `TokenKind :: enum {
|
|
if
|
|
else
|
|
return
|
|
}
|
|
Token :: union(TokenKind) {
|
|
if i32
|
|
else void
|
|
return i32
|
|
}
|
|
kind func(value bool) TokenKind {
|
|
if value {
|
|
return .if
|
|
}
|
|
return TokenKind.else
|
|
}
|
|
main func() i32 {
|
|
first TokenKind = kind(true)
|
|
second TokenKind = .return
|
|
a Token = Token{ if = 1 }
|
|
b Token = Token{ else }
|
|
c Token = .return{2}
|
|
total i32 = a.if + c.return
|
|
match first {
|
|
.if: total = total + 1
|
|
.else: total = total + 2
|
|
.return: total = total + 3
|
|
}
|
|
match b {
|
|
.if |value|: total = total + value
|
|
.else: total = total + 4
|
|
.return |value|: total = total + value
|
|
}
|
|
_ = second
|
|
return total
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, len(llvm_text) > 0)
|
|
}
|
|
|
|
@(test)
|
|
keyword_names_remain_invalid_for_struct_fields :: proc(t: ^testing.T) {
|
|
text := `Bad :: struct {
|
|
if i32
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "expected a struct field name")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
unbacked_enum_uses_global_u16_backing :: proc(t: ^testing.T) {
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
strings.write_string(&builder, "Large :: enum {\n")
|
|
for index in 0..<257 {
|
|
fmt.sbprintf(&builder, "value_%d\n", index)
|
|
}
|
|
strings.write_string(&builder, "}\nmain func() void {}\n")
|
|
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
large := types.find_named(&module.type_store, 0, u32(symbol.intern(&symbols, "Large")))
|
|
item, ok := types.node(&module.type_store, large)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, ok)
|
|
testing.expect_value(t, item.child, types.U16)
|
|
testing.expect_value(t, len(types.enum_members_for(&module.type_store, large)), 257)
|
|
}
|
|
|
|
@(test)
|
|
native_enum_invalid_declarations_and_operations_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := `Empty :: enum {}
|
|
Dense :: enum {
|
|
zero = 0
|
|
one
|
|
}
|
|
BadBacking :: enum(f32) {
|
|
value
|
|
}
|
|
Duplicate :: enum(u8) {
|
|
value
|
|
value
|
|
}
|
|
Jumbled :: enum(i8) {
|
|
second = 2
|
|
first = 1
|
|
}
|
|
Overflow :: enum(u8) {
|
|
value = 256
|
|
}
|
|
Other :: enum {
|
|
value
|
|
}
|
|
foreign c_func(value Dense) void
|
|
allowed c_func(value Overflow) Overflow
|
|
main func() void {
|
|
dense Dense = Other.value
|
|
_ = Dense.zero + Dense.one
|
|
_ = Dense.zero < Dense.one
|
|
_ = Dense.missing
|
|
_ = .zero
|
|
_ = dense
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_empty := false
|
|
found_unbacked_value := false
|
|
found_backing := false
|
|
found_duplicate := false
|
|
found_order := false
|
|
found_overflow := false
|
|
found_foreign := false
|
|
found_conversion := false
|
|
found_arithmetic := false
|
|
found_comparison := false
|
|
found_member := false
|
|
found_context := false
|
|
for diagnostic in diagnostics.items {
|
|
found_empty = found_empty || strings.contains(diagnostic.message, "require at least one member")
|
|
found_unbacked_value = found_unbacked_value || strings.contains(diagnostic.message, "explicit enum values require a backing type")
|
|
found_backing = found_backing || strings.contains(diagnostic.message, "requires a concrete integer backing type")
|
|
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate enum member")
|
|
found_order = found_order || strings.contains(diagnostic.message, "strictly increasing")
|
|
found_overflow = found_overflow || strings.contains(diagnostic.message, "does not fit in u8")
|
|
found_foreign = found_foreign || strings.contains(diagnostic.message, "requires concrete parameter types")
|
|
found_conversion = found_conversion || strings.contains(diagnostic.message, "cannot implicitly convert")
|
|
found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
|
|
found_comparison = found_comparison || strings.contains(diagnostic.message, "enum values only support")
|
|
found_member = found_member || strings.contains(diagnostic.message, "unknown enum member")
|
|
found_context = found_context || strings.contains(diagnostic.message, "requires an enum context")
|
|
}
|
|
testing.expect(t, found_empty)
|
|
testing.expect(t, found_unbacked_value)
|
|
testing.expect(t, found_backing)
|
|
testing.expect(t, found_duplicate)
|
|
testing.expect(t, found_order)
|
|
testing.expect(t, found_overflow)
|
|
testing.expect(t, found_foreign)
|
|
testing.expect(t, found_conversion)
|
|
testing.expect(t, found_arithmetic)
|
|
testing.expect(t, found_comparison)
|
|
testing.expect(t, found_member)
|
|
testing.expect(t, found_context)
|
|
}
|
|
|
|
@(test)
|
|
native_enums_compile_and_run_across_packages :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-enums"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/enums", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
translate_c_emits_native_bindings_and_round_trips :: proc(t: ^testing.T) {
|
|
result := cimport.init_result(context.allocator)
|
|
|
|
// type table: [0]=c_int, [1]=c_ulong, [2]=Record(Pair),
|
|
// [3]=Function(c_int)->c_int, [4]=Pointer->Function (callback),
|
|
// [5]=void, [6]=Pointer->void
|
|
append(&result.types, cimport.Type{kind = .C_Int, child = cimport.INVALID_TYPE})
|
|
append(&result.types, cimport.Type{kind = .C_Ulong, child = cimport.INVALID_TYPE})
|
|
append(&result.types, cimport.Type{kind = .Record, record = 0, child = cimport.INVALID_TYPE})
|
|
func_params := []cimport.Type_Id{cimport.Type_Id(0)}
|
|
append(&result.types, cimport.Type{kind = .Function, params = func_params, child = cimport.Type_Id(0)})
|
|
append(&result.types, cimport.Type{kind = .Pointer, child = cimport.Type_Id(3)})
|
|
append(&result.types, cimport.Type{kind = .Void, child = cimport.INVALID_TYPE})
|
|
append(&result.types, cimport.Type{kind = .Pointer, child = cimport.Type_Id(5), mutable = true})
|
|
|
|
// record 0: Pair { left c_int; right c_int }
|
|
pair_fields: [dynamic]cimport.Field
|
|
append(&pair_fields, cimport.Field{name = "left", type = cimport.Type_Id(0)})
|
|
append(&pair_fields, cimport.Field{name = "right", type = cimport.Type_Id(0)})
|
|
append(&result.records, cimport.Record{name = "Pair", fields = pair_fields, kind = .Struct, complete = true})
|
|
|
|
// record 1: union Choice -> commented (no native spelling)
|
|
choice_fields: [dynamic]cimport.Field
|
|
append(&choice_fields, cimport.Field{name = "tag", type = cimport.Type_Id(0)})
|
|
append(&result.records, cimport.Record{name = "Choice", fields = choice_fields, kind = .Union, complete = true})
|
|
|
|
// record 2: incomplete struct -> opaque
|
|
append(&result.records, cimport.Record{name = "Handle", kind = .Struct, complete = false})
|
|
|
|
// typedef aliases: a scalar, a callback function pointer, and a C void pointer
|
|
append(&result.aliases, cimport.Alias{name = "Size", type = cimport.Type_Id(1)})
|
|
append(&result.aliases, cimport.Alias{name = "Mapper", type = cimport.Type_Id(4)})
|
|
append(&result.aliases, cimport.Alias{name = "RawPtr", type = cimport.Type_Id(6)})
|
|
|
|
add_params := []cimport.Type_Id{cimport.Type_Id(0), cimport.Type_Id(0)}
|
|
add_param_names := []string{"a", "b"}
|
|
append(&result.functions, cimport.Function{name = "imported_add", params = add_params, param_names = add_param_names, result = cimport.Type_Id(0)})
|
|
raw_params := []cimport.Type_Id{cimport.Type_Id(6)}
|
|
raw_param_names := []string{"ptr"}
|
|
append(&result.functions, cimport.Function{name = "consume_raw", params = raw_params, param_names = raw_param_names, result = cimport.Type_Id(5)})
|
|
|
|
append(&result.macros, cimport.Macro_Constant{
|
|
name = "MAX_LEN",
|
|
type = cimport.Type_Id(0),
|
|
value = {kind = .Integer, type = cimport.Type_Id(0), integer = 256},
|
|
})
|
|
|
|
// external variable -> commented (no native spelling)
|
|
append(&result.variables, cimport.Variable{name = "some_global", type = cimport.Type_Id(0), mutable = true})
|
|
|
|
result.available = true
|
|
|
|
output := translatec.emit(&result, "test.h")
|
|
defer delete(output)
|
|
defer {
|
|
delete(result.types)
|
|
delete(result.records)
|
|
delete(result.aliases)
|
|
delete(result.functions)
|
|
delete(result.variables)
|
|
delete(result.macros)
|
|
delete(pair_fields)
|
|
delete(choice_fields)
|
|
}
|
|
|
|
testing.expect(t, strings.contains(output, "Pair :: c_struct {"))
|
|
testing.expect(t, strings.contains(output, "\tleft c_int"))
|
|
testing.expect(t, strings.contains(output, "\tright c_int"))
|
|
testing.expect(t, strings.contains(output, "Size :: alias c_ulong"))
|
|
// Function-pointer types carry no parameter names, so the callback renders `_`.
|
|
testing.expect(t, strings.contains(output, "Mapper :: alias ?*c_func(_ c_int) c_int"))
|
|
testing.expect(t, strings.contains(output, "RawPtr :: alias ?*mut anyopaque"))
|
|
testing.expect(t, strings.contains(output, "Handle :: opaque"))
|
|
// Real C parameter names are used when present.
|
|
testing.expect(t, strings.contains(output, "imported_add c_func(a c_int, b c_int) c_int"))
|
|
testing.expect(t, strings.contains(output, "consume_raw c_func(ptr ?*mut anyopaque) void"))
|
|
testing.expect(t, strings.contains(output, "MAX_LEN c_int :: 256"))
|
|
testing.expect(t, strings.contains(output, "# unsupported in bindings: C union 'Choice'"))
|
|
testing.expect(t, strings.contains(output, "# unsupported in bindings: external variable 'some_global'"))
|
|
|
|
// Round-trip: the emitted source must lex + parse with zero diagnostics.
|
|
// This guards render_type against drift from loader.translate_c_type and
|
|
// exercises the new `alias` declaration syntax.
|
|
source_file := source.Source{path = "bindings.bro", text = output}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
translate_c_package_deduplicates_compatible_declarations :: proc(t: ^testing.T) {
|
|
first := cimport.init_result(context.allocator)
|
|
second := cimport.init_result(context.allocator)
|
|
defer {
|
|
delete(first.types)
|
|
delete(first.records[0].fields)
|
|
delete(first.records)
|
|
delete(first.functions)
|
|
delete(first.macros)
|
|
delete(second.types)
|
|
delete(second.records[0].fields)
|
|
delete(second.records)
|
|
delete(second.functions)
|
|
delete(second.macros)
|
|
}
|
|
|
|
append(&first.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
|
|
append(&first.types, cimport.Type{kind=.Record, record=0, child=cimport.INVALID_TYPE})
|
|
append(&first.types, cimport.Type{kind=.Pointer, child=1, mutable=true})
|
|
append(&second.types, ..first.types[:])
|
|
first_fields: [dynamic]cimport.Field
|
|
second_fields: [dynamic]cimport.Field
|
|
append(&first_fields, cimport.Field{name="value", type=0})
|
|
append(&second_fields, cimport.Field{name="value", type=0})
|
|
append(&first.records, cimport.Record{
|
|
identity="shared-anonymous-record",
|
|
kind=.Struct,
|
|
complete=true,
|
|
fields=first_fields,
|
|
})
|
|
append(&second.records, cimport.Record{
|
|
identity="shared-anonymous-record",
|
|
kind=.Struct,
|
|
complete=true,
|
|
fields=second_fields,
|
|
})
|
|
first_params := [?]cimport.Type_Id{0}
|
|
second_params := [?]cimport.Type_Id{0}
|
|
use_record_params := [?]cimport.Type_Id{2}
|
|
first_names := [?]string{"left"}
|
|
second_names := [?]string{"right"}
|
|
append(&first.functions, cimport.Function{
|
|
name="shared",
|
|
params=first_params[:],
|
|
param_names=first_names[:],
|
|
result=0,
|
|
})
|
|
append(&second.functions, cimport.Function{
|
|
name="shared",
|
|
params=second_params[:],
|
|
param_names=second_names[:],
|
|
result=0,
|
|
})
|
|
append(&second.functions, cimport.Function{
|
|
name="use_record",
|
|
params=use_record_params[:],
|
|
result=0,
|
|
})
|
|
append(&first.macros, cimport.Macro_Constant{
|
|
name="SHARED_VALUE",
|
|
type=0,
|
|
value={kind=.Integer, type=0, integer=7},
|
|
})
|
|
append(&second.macros, first.macros[0])
|
|
|
|
inputs := [?]translatec.Package_Input{
|
|
{result=&first, header="first.h", name="first.bro"},
|
|
{result=&second, header="second.h", name="second.bro"},
|
|
}
|
|
outputs, generation_error := translatec.emit_package(inputs[:])
|
|
defer translatec.destroy_package_outputs(outputs)
|
|
defer delete(generation_error)
|
|
testing.expect_value(t, generation_error, "")
|
|
testing.expect_value(t, len(outputs), 2)
|
|
if len(outputs) == 2 {
|
|
combined := fmt.tprintf("%s%s", outputs[0].source, outputs[1].source)
|
|
testing.expect_value(t, count_substring_occurrences(combined, "shared c_func("), 1)
|
|
testing.expect_value(t, count_substring_occurrences(combined, "SHARED_VALUE c_int :: 7"), 1)
|
|
testing.expect(t, strings.contains(outputs[0].source, "__c_first_bro_record_0 :: c_struct"))
|
|
testing.expect(t, strings.contains(outputs[1].source, "use_record c_func(_ ?*mut __c_first_bro_record_0) c_int"))
|
|
}
|
|
|
|
second.macros[0].value.integer = 8
|
|
conflicting, conflict_error := translatec.emit_package(inputs[:])
|
|
defer translatec.destroy_package_outputs(conflicting)
|
|
defer delete(conflict_error)
|
|
testing.expect_value(t, len(conflicting), 0)
|
|
testing.expect(t, strings.contains(conflict_error, "conflicting generated C declaration 'SHARED_VALUE'"))
|
|
}
|
|
|
|
@(test)
|
|
translate_c_package_cli_requires_unambiguous_outputs :: proc(t: ^testing.T) {
|
|
testing.expect_value(t, run_translate_c([]string{
|
|
"brolang", "--translate-c", "first.h", "second.h",
|
|
}), 2)
|
|
testing.expect_value(t, run_translate_c([]string{
|
|
"brolang", "--translate-c", "a/same.h", "b/same.h", "--output-dir", "/tmp/unused",
|
|
}), 2)
|
|
}
|
|
|
|
@(test)
|
|
sink_named_parameters_are_allowed_and_not_duplicates :: proc(t: ^testing.T) {
|
|
// Generated bindings use `_` for unnamed C params; the parser must accept it and
|
|
// the checker must not flag repeated `_` as duplicate parameters.
|
|
text := `foo c_func(_ c_int, _ c_int) c_int
|
|
main func() void {
|
|
_ = foo(1, 2)
|
|
}
|
|
`
|
|
source_file := source.Source{path = "test.bro", text = text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
for diagnostic in diagnostics.items {
|
|
testing.expect(t, !strings.contains(diagnostic.message, "duplicate parameter"))
|
|
testing.expect(t, !strings.contains(diagnostic.message, "expected parameter name"))
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_resolves_signed_const_chain :: proc(t: ^testing.T) {
|
|
text := `X :: 1000
|
|
Y int :: X
|
|
Z i32 :: Y
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
// The concrete i32 on Z flows backward through Y to the open constant X, so all
|
|
// three resolve to i32 instead of X/Y staying at the literal's smallest signed type.
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, types.equal(hir_module.globals[0].type, types.I32))
|
|
testing.expect(t, types.equal(hir_module.globals[1].type, types.I32))
|
|
testing.expect(t, types.equal(hir_module.globals[2].type, types.I32))
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_open_constants_adopt_unsigned_demand :: proc(t: ^testing.T) {
|
|
text := `A :: 10
|
|
B u16 :: A
|
|
P :: 10
|
|
R u32 :: P
|
|
N :: 42
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
// An open constant is sign-agnostic until used: it adopts the unsigned family a use
|
|
// demands (the literal's signed default would block this). Unconstrained N defaults.
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, types.equal(hir_module.globals[0].type, types.U16)) // A
|
|
testing.expect(t, types.equal(hir_module.globals[1].type, types.U16)) // B
|
|
testing.expect(t, types.equal(hir_module.globals[2].type, types.U32)) // P
|
|
testing.expect(t, types.equal(hir_module.globals[3].type, types.U32)) // R
|
|
testing.expect(t, types.equal(hir_module.globals[4].type, types.I8)) // N
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_rejects_constant_that_does_not_fit_demand :: proc(t: ^testing.T) {
|
|
text := `BIG :: 100000
|
|
C u8 :: BIG
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
// 100000 does not fit u8, so the demand is rejected, BIG defaults to i32, and the
|
|
// genuine mismatch surfaces at the use's boundary coercion.
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_does_not_cross_call_boundaries :: proc(t: ^testing.T) {
|
|
text := `echo func(p int) int { return p }
|
|
A :: 10
|
|
R u32 :: echo(A)
|
|
main func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
// The u32 demand on R must not flow through echo into A (that is L3, deferred). A
|
|
// stays at its default i8, so the call result fails to coerce to u32.
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i8 to u32")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_resolves_locals_like_globals :: proc(t: ^testing.T) {
|
|
text := `take_u16 func(_ u16) void {}
|
|
get func() u16 {
|
|
c :: 10
|
|
return c
|
|
}
|
|
main func() void {
|
|
x :: 1000
|
|
y int :: x
|
|
z i32 :: y
|
|
a :: 10
|
|
b u16 :: a
|
|
n :: 5
|
|
take_u16(n)
|
|
_ = z
|
|
_ = b
|
|
_ = get()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
// The same backward propagation works for locals: a constant local adopts the
|
|
// unsigned/wider type a later use demands (declaration, call argument, or return),
|
|
// so none of these need an explicit annotation. Without it, i8->u16/u32 would error.
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_demand_from_function_body_reaches_global :: proc(t: ^testing.T) {
|
|
text := `take_u16 func(_ u16) void {}
|
|
G :: 10
|
|
main func() void {
|
|
take_u16(G)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
// A demand originating inside a function body (passing G to a u16 parameter) flows
|
|
// back to the open-constant global G, resolving it to u16.
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, types.equal(hir_module.globals[0].type, types.U16))
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_flows_through_compound_assignment :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
s :: 5
|
|
v u16 = 0
|
|
v += s
|
|
_ = v
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
// `s` is used only as the RHS of `v += s`. The assignment target's type (u16) is
|
|
// demanded backward onto `s`, resolving the open constant; without it the compound
|
|
// assignment would report "arithmetic requires compatible numeric operands".
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_resolves_open_global_arithmetic_across_uses :: proc(t: ^testing.T) {
|
|
text := `take_ci func(_ c_int) void {}
|
|
W :: 800
|
|
Z :: 40
|
|
STEP :: 5
|
|
main func() void {
|
|
take_ci(W)
|
|
x int = W - Z
|
|
take_ci(Z)
|
|
x += STEP
|
|
take_ci(x)
|
|
_ = x
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
// `W - Z` mixes two open-constant globals while `Z`'s c_int use comes only after the
|
|
// arithmetic, and `STEP` is used only in a compound assignment. With deferred defaulting
|
|
// an undemanded open global stays typeless during the fixpoint instead of leaking a
|
|
// provisional i16 default, so W/Z/STEP all resolve to c_int. Previously this reported
|
|
// "arithmetic requires compatible numeric operands" / "cannot implicitly convert i16 to c_int".
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
for global in hir_module.globals {
|
|
name := symbol.resolve(&symbols, global.name)
|
|
if name == "W" || name == "Z" || name == "STEP" {
|
|
testing.expect(t, types.equal(global.type, types.C_INT))
|
|
}
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testing.T) {
|
|
text := `main func() void {
|
|
big :: 100000
|
|
c u8 :: big
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
// 100000 does not fit u8, so big keeps its i32 default and the use errors.
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_flows_through_numeric_arithmetic :: proc(t: ^testing.T) {
|
|
text := `take_u16 func(_ u16) void {}
|
|
take_f32 func(_ f32) void {}
|
|
G :: 10
|
|
H u16 :: G + 2
|
|
GF :: 1.5
|
|
HF f32 :: GF + 2.5
|
|
CG :: 5
|
|
CFG :: 1.0
|
|
get func() f32 {
|
|
seed f32 :: 2.0
|
|
c :: seed + 3.0
|
|
d :: 4.0 + seed
|
|
return c + d
|
|
}
|
|
main func() void {
|
|
a :: 10
|
|
b u16 :: a + 2
|
|
x :: 1.5
|
|
y f32 :: x + 2.5
|
|
z f32 :: 2.5 + x
|
|
call_i :: 7
|
|
call_f :: 1.25
|
|
take_u16(call_i + 3)
|
|
take_f32(call_f + 3.0)
|
|
take_u16(CG + 1)
|
|
take_f32(CFG + 1.0)
|
|
_ = b
|
|
_ = y
|
|
_ = z
|
|
_ = H
|
|
_ = HF
|
|
_ = get()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
|
|
global_ok := 0
|
|
for global in hir_module.globals {
|
|
name := symbol.resolve(&symbols, global.name)
|
|
switch name {
|
|
case "G", "H", "CG":
|
|
global_ok += 1 if types.equal(global.type, types.U16) else 0
|
|
case "GF", "HF", "CFG":
|
|
global_ok += 1 if types.equal(global.type, types.F32) else 0
|
|
}
|
|
}
|
|
testing.expect_value(t, global_ok, 6)
|
|
|
|
main_ok := 0
|
|
get_ok := false
|
|
for function in hir_module.functions {
|
|
name := symbol.resolve(&symbols, function.name)
|
|
if name == "get" {
|
|
get_ok = types.equal(function.result, types.F32)
|
|
for local in function.locals {
|
|
local_name := symbol.resolve(&symbols, local.name)
|
|
if local_name == "c" || local_name == "d" {
|
|
main_ok += 1 if types.equal(local.type, types.F32) else 0
|
|
}
|
|
}
|
|
} else if name == "main" {
|
|
for local in function.locals {
|
|
local_name := symbol.resolve(&symbols, local.name)
|
|
switch local_name {
|
|
case "a", "call_i":
|
|
main_ok += 1 if types.equal(local.type, types.U16) else 0
|
|
case "x", "call_f":
|
|
main_ok += 1 if types.equal(local.type, types.F32) else 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
testing.expect(t, get_ok)
|
|
testing.expect_value(t, main_ok, 6)
|
|
}
|
|
|
|
@(test)
|
|
contextual_inference_rejects_non_fitting_arithmetic_demand :: proc(t: ^testing.T) {
|
|
text := `BIG :: 100000
|
|
C u8 :: BIG + 1
|
|
main func() void {
|
|
_ = C
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
missing_qualified_signature_symbol_reports_one_root_error :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-root-diagnostic"
|
|
defer _ = os2.remove_all(directory)
|
|
_ = os2.remove_all(directory)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
token_text := `Token :: struct { start int }
|
|
`
|
|
lexer_text := `scan func(cursor usize) void ! missing.Error {
|
|
token Token = Token{start = cursor}
|
|
_ = token
|
|
}
|
|
`
|
|
main_text := `main func() void {
|
|
scan(1) catch |_| { return }
|
|
}
|
|
`
|
|
testing.expect(t, os.write_entire_file(
|
|
"/tmp/brolang-test-root-diagnostic/token.bro",
|
|
transmute([]byte)token_text,
|
|
))
|
|
testing.expect(t, os.write_entire_file(
|
|
"/tmp/brolang-test-root-diagnostic/lexer.bro",
|
|
transmute([]byte)lexer_text,
|
|
))
|
|
testing.expect(t, os.write_entire_file(
|
|
"/tmp/brolang-test-root-diagnostic/main.bro",
|
|
transmute([]byte)main_text,
|
|
))
|
|
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
module, loaded := loader.load(directory, &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&module)
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'")
|
|
formatted := source.format(&diagnostics, source.Diagnostic_Id(0))
|
|
defer delete(formatted)
|
|
testing.expect(t, strings.contains(formatted, "/lexer.bro:1:32"))
|
|
testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol"))
|
|
testing.expect(t, !strings.contains(formatted, "fallible expression"))
|
|
testing.expect(t, !strings.contains(formatted, "must be consumed"))
|
|
testing.expect(t, !strings.contains(formatted, "could not resolve the 'int' constraint"))
|
|
}
|
|
|
|
@(test)
|
|
poisoned_global_and_local_types_do_not_create_inference_fallbacks :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-poisoned-declarations"
|
|
defer _ = os2.remove_all(directory)
|
|
_ = os2.remove_all(directory)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
text := `bad missing.Global :: 1
|
|
main func() void {
|
|
value absent.Local = 1
|
|
_ = value
|
|
}
|
|
`
|
|
testing.expect(t, os.write_entire_file(
|
|
"/tmp/brolang-test-poisoned-declarations/main.bro",
|
|
transmute([]byte)text,
|
|
))
|
|
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
module, loaded := loader.load(directory, &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&module)
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, len(diagnostics.items), 2)
|
|
unknown_missing := false
|
|
unknown_absent := false
|
|
for diagnostic in diagnostics.items {
|
|
unknown_missing = unknown_missing || diagnostic.message == "unknown symbol 'missing'"
|
|
unknown_absent = unknown_absent || diagnostic.message == "unknown symbol 'absent'"
|
|
testing.expect(t, !strings.contains(diagnostic.message, "could not resolve"))
|
|
testing.expect(t, !strings.contains(diagnostic.message, "could not infer"))
|
|
}
|
|
testing.expect(t, unknown_missing)
|
|
testing.expect(t, unknown_absent)
|
|
}
|
|
|
|
named_record_field_type :: proc(
|
|
module: ^hir.Module,
|
|
symbols: ^symbol.Table,
|
|
record_name, field_name: string,
|
|
) -> (types.Type, bool) {
|
|
record := types.find_named(&module.types, 0, u32(symbol.intern(symbols, record_name)))
|
|
if !types.is_record(record, &module.types) {
|
|
return types.INVALID, false
|
|
}
|
|
field_symbol := symbol.intern(symbols, field_name)
|
|
for field in types.fields_for(&module.types, record) {
|
|
if field.name == u32(field_symbol) {
|
|
return field.type, true
|
|
}
|
|
}
|
|
return types.INVALID, false
|
|
}
|
|
|
|
@(test)
|
|
native_record_constraint_fields_resolve_program_wide :: proc(t: ^testing.T) {
|
|
text := `Token :: struct { start int }
|
|
Backward :: struct { start int }
|
|
Wide :: struct { value int }
|
|
Literal :: struct { value int }
|
|
Measurement :: struct {
|
|
ratio float
|
|
span range
|
|
}
|
|
Payload :: union { count int }
|
|
|
|
take_usize func(value usize) usize { return value }
|
|
|
|
exercise func(cursor usize, narrow i8, wider i16, count i32) i32 {
|
|
token Token = Token{start = cursor}
|
|
wide Wide = Wide{value = narrow}
|
|
wide.value = wider
|
|
small Literal = Literal{value = 1}
|
|
large Literal = Literal{value = 1000}
|
|
backward Backward = Backward{start = 1}
|
|
measurement Measurement = Measurement{ratio = 1.5, span = 0..3}
|
|
payload Payload = Payload{count = count}
|
|
_ = token.start
|
|
_ = wide.value
|
|
_ = small.value
|
|
_ = large.value
|
|
_ = take_usize(backward.start)
|
|
_ = measurement.ratio
|
|
_ = measurement.span
|
|
_ = payload.count
|
|
return 0
|
|
}
|
|
|
|
main func() i32 { return exercise(7, 1, 1000, 3) }
|
|
`
|
|
source_file := source.Source{path="record_constraints.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
token_start, token_ok := named_record_field_type(&hir_module, &symbols, "Token", "start")
|
|
backward_start, backward_ok := named_record_field_type(&hir_module, &symbols, "Backward", "start")
|
|
wide_value, wide_ok := named_record_field_type(&hir_module, &symbols, "Wide", "value")
|
|
literal_value, literal_ok := named_record_field_type(&hir_module, &symbols, "Literal", "value")
|
|
ratio, ratio_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "ratio")
|
|
span, span_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "span")
|
|
count, count_ok := named_record_field_type(&hir_module, &symbols, "Payload", "count")
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, token_ok && backward_ok && wide_ok && literal_ok && ratio_ok && span_ok && count_ok)
|
|
testing.expect_value(t, token_start, types.USIZE)
|
|
testing.expect_value(t, backward_start, types.USIZE)
|
|
testing.expect_value(t, wide_value, types.I16)
|
|
testing.expect_value(t, literal_value, types.I16)
|
|
testing.expect_value(t, ratio, types.F64)
|
|
testing.expect(t, types.is_range(span, &hir_module.types))
|
|
testing.expect_value(t, types.child_type(span, &hir_module.types), types.I8)
|
|
testing.expect_value(t, count, types.I32)
|
|
}
|
|
|
|
@(test)
|
|
native_record_int_field_compiles_and_runs_as_usize :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-record-field-usize"
|
|
main_path := "/tmp/brolang-test-record-field-usize/main.bro"
|
|
output := "/tmp/brolang-test-record-field-usize-output"
|
|
text := `Token :: struct { start int }
|
|
main func() i32 {
|
|
cursor usize = 7
|
|
token Token = Token{start = cursor}
|
|
if (token.start != cursor) return 1
|
|
return 0
|
|
}
|
|
`
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
native_record_constraint_fields_report_unresolved_and_conflicting_demands :: proc(t: ^testing.T) {
|
|
cases := [2]struct {
|
|
text: string,
|
|
needle: string,
|
|
}{
|
|
{
|
|
text = `Token :: struct { start int }
|
|
main func() void {}
|
|
`,
|
|
needle = "could not resolve the 'int' constraint for field 'Token.start'",
|
|
},
|
|
{
|
|
text = `Token :: struct { start int }
|
|
use func(signed i32, unsigned usize) void {
|
|
a Token = Token{start = signed}
|
|
b Token = Token{start = unsigned}
|
|
_ = a
|
|
_ = b
|
|
}
|
|
main func() void { use(1, 2) }
|
|
`,
|
|
needle = "conflicting types i32 and usize for field 'Token.start' declared as 'int'",
|
|
},
|
|
}
|
|
for test_case in cases {
|
|
source_file := source.Source{path="bad_record_constraint.bro", text=test_case.text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
symbols := symbol.init_table()
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
ir_module := lower.lower(&hir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
|
|
matching := 0
|
|
for diagnostic in diagnostics.items {
|
|
matching += 1 if strings.contains(diagnostic.message, test_case.needle) else 0
|
|
}
|
|
testing.expect_value(t, matching, 1)
|
|
testing.expect(t, len(llvm_text) > 0)
|
|
|
|
delete(llvm_text)
|
|
ir.destroy_module(&ir_module)
|
|
hir.destroy_module(&hir_module)
|
|
ast.destroy_module(&ast_module)
|
|
delete(stream.items)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
}
|
|
|
|
directory := "/tmp/brolang-test-record-field-conflict"
|
|
main_path := "/tmp/brolang-test-record-field-conflict/main.bro"
|
|
output := "/tmp/brolang-test-record-field-conflict-output"
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)cases[1].text))
|
|
testing.expect_value(t, compiler_core.compile_package(directory, output), 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
constraint_fields_remain_rejected_outside_named_native_records :: proc(t: ^testing.T) {
|
|
text := `BadC :: c_struct { value int }
|
|
BadNested :: struct { values []int }
|
|
|
|
make_type func() type {
|
|
return struct { value int }
|
|
}
|
|
|
|
main func() void {
|
|
Generated :: @make_type()
|
|
_ = Generated
|
|
}
|
|
`
|
|
source_file := source.Source{path="excluded_record_constraints.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
record_errors := 0
|
|
anonymous_error := false
|
|
for diagnostic in diagnostics.items {
|
|
record_errors += 1 if strings.contains(diagnostic.message, "record fields must have runtime value types") else 0
|
|
anonymous_error = anonymous_error || strings.contains(diagnostic.message, "anonymous struct field 'value' requires a concrete runtime type")
|
|
}
|
|
c_field, c_ok := named_record_field_type(&hir_module, &symbols, "BadC", "value")
|
|
nested_field, nested_ok := named_record_field_type(&hir_module, &symbols, "BadNested", "values")
|
|
testing.expect_value(t, record_errors, 1)
|
|
testing.expect(t, anonymous_error)
|
|
testing.expect(t, c_ok && nested_ok)
|
|
testing.expect_value(t, c_field, types.INT)
|
|
testing.expect(t, types.is_slice(nested_field, &hir_module.types))
|
|
testing.expect_value(t, types.child_type(nested_field, &hir_module.types), types.INT)
|
|
}
|
|
|
|
@(test)
|
|
parser_records_native_tests_and_test_imports :: proc(t: ^testing.T) {
|
|
text := `test import "../math"
|
|
|
|
addition test {
|
|
return
|
|
}
|
|
`
|
|
source_file := source.Source{path="tests.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(module.imports), 1)
|
|
testing.expect(t, module.imports[0].test_only)
|
|
testing.expect_value(t, module.imports[0].alias, symbol.INVALID)
|
|
testing.expect_value(t, len(module.functions), 1)
|
|
testing.expect(t, module.functions[0].test)
|
|
testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "addition")
|
|
}
|
|
|
|
@(test)
|
|
native_test_framework_discovers_reports_and_preserves_main :: proc(t: ^testing.T) {
|
|
directory := "/tmp/brolang-test-native-framework"
|
|
root := "/tmp/brolang-test-native-framework/root"
|
|
dependency := "/tmp/brolang-test-native-framework/dependency"
|
|
root_path := "/tmp/brolang-test-native-framework/root/main.bro"
|
|
dependency_path := "/tmp/brolang-test-native-framework/dependency/math.bro"
|
|
test_output := "/tmp/brolang-test-native-framework-tests"
|
|
app_output := "/tmp/brolang-test-native-framework-app"
|
|
_ = os2.remove_all(directory)
|
|
defer _ = os2.remove_all(directory)
|
|
defer _ = os.remove(test_output)
|
|
defer _ = os.remove(app_output)
|
|
testing.expect(t, os2.make_directory_all(root) == nil)
|
|
testing.expect(t, os2.make_directory_all(dependency) == nil)
|
|
|
|
root_text := `testing :: import "@std/testing"
|
|
|
|
test import "../dependency"
|
|
|
|
main func() i32 { return 77 }
|
|
|
|
root_passes test {
|
|
try testing.expect(true)
|
|
}
|
|
|
|
root_fails test {
|
|
try testing.expect_equal(42, 41)
|
|
}
|
|
|
|
root_continues test {
|
|
try testing.expect(true)
|
|
}
|
|
`
|
|
dependency_text := `testing :: import "@std/testing"
|
|
|
|
dependency_passes test {
|
|
try testing.expect(true)
|
|
}
|
|
`
|
|
testing.expect(t, os.write_entire_file(root_path, transmute([]byte)root_text))
|
|
testing.expect(t, os.write_entire_file(dependency_path, transmute([]byte)dependency_text))
|
|
|
|
app_status := compiler_core.compile_package(
|
|
root, app_output, nil, target.DEFAULT, cimport.Options{}, ".",
|
|
)
|
|
testing.expect_value(t, app_status, 0)
|
|
app_state := run_executable(app_output)
|
|
testing.expect_value(t, app_state.exit_code, 77)
|
|
|
|
test_status := compiler_core.compile_package(
|
|
root, test_output, nil, target.DEFAULT, cimport.Options{}, ".", .Test,
|
|
)
|
|
testing.expect_value(t, test_status, 0)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{test_output}},
|
|
context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
output := string(stderr)
|
|
testing.expect_value(t, state.exit_code, 1)
|
|
testing.expect(t, strings.contains(output, "PASS root.root_passes"))
|
|
testing.expect(t, strings.contains(output, "FAIL root.root_fails"))
|
|
testing.expect(t, strings.contains(output, "expected 42, found 41"))
|
|
testing.expect(t, strings.contains(output, "PASS root.root_continues"))
|
|
testing.expect(t, strings.contains(output, "PASS dependency.dependency_passes"))
|
|
testing.expect(t, strings.contains(output, root_path))
|
|
testing.expect(t, strings.contains(output, "3 passed, 1 failed"))
|
|
}
|
|
|
|
@(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)
|
|
}
|