6915 lines
238 KiB
Odin
6915 lines
238 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)
|
|
compact_ids_reserve_invalid_values_and_preserve_layout :: proc(t: ^testing.T) {
|
|
testing.expect_value(t, size_of(source.Span), 12)
|
|
testing.expect_value(t, size_of(token.Token), 24)
|
|
testing.expect(t, size_of(ast.Expr) <= 64)
|
|
testing.expect(t, size_of(hir.Expr) <= 88)
|
|
testing.expect(t, size_of(ir.Instruction) <= 88)
|
|
testing.expect_value(t, size_of(types.Type), 4)
|
|
|
|
source_index, source_ok := source.source_index(source.Source_Id(0), 1)
|
|
testing.expect_value(t, source_index, 0)
|
|
testing.expect(t, source_ok)
|
|
_, source_invalid := source.source_index(source.INVALID_SOURCE, 1)
|
|
testing.expect(t, !source_invalid)
|
|
_, source_out_of_bounds := source.source_index(source.Source_Id(1), 1)
|
|
testing.expect(t, !source_out_of_bounds)
|
|
diagnostic_index, diagnostic_ok := source.diagnostic_index(source.Diagnostic_Id(0), 1)
|
|
testing.expect_value(t, diagnostic_index, 0)
|
|
testing.expect(t, diagnostic_ok)
|
|
_, diagnostic_invalid := source.diagnostic_index(source.INVALID_DIAGNOSTIC, 1)
|
|
testing.expect(t, !diagnostic_invalid)
|
|
|
|
expr_index, expr_ok := ast.index(ast.Expr_Id(0), ast.INVALID_EXPR, 1)
|
|
testing.expect_value(t, expr_index, 0)
|
|
testing.expect(t, expr_ok)
|
|
_, expr_invalid := ast.index(ast.INVALID_EXPR, ast.INVALID_EXPR, 1)
|
|
testing.expect(t, !expr_invalid)
|
|
|
|
function_index, function_ok := hir.index(hir.Function_Id(0), hir.INVALID_FUNCTION, 1)
|
|
testing.expect_value(t, function_index, 0)
|
|
testing.expect(t, function_ok)
|
|
_, function_invalid := hir.index(hir.INVALID_FUNCTION, hir.INVALID_FUNCTION, 1)
|
|
testing.expect(t, !function_invalid)
|
|
|
|
instruction_index, instruction_ok := ir.index(ir.Instruction_Id(0), ir.INVALID_INSTRUCTION, 1)
|
|
testing.expect_value(t, instruction_index, 0)
|
|
testing.expect(t, instruction_ok)
|
|
_, instruction_invalid := ir.index(ir.INVALID_INSTRUCTION, ir.INVALID_INSTRUCTION, 1)
|
|
testing.expect(t, !instruction_invalid)
|
|
|
|
spec_index, spec_ok := checker.spec_index(checker.Spec_Id(0), 1)
|
|
testing.expect_value(t, spec_index, 0)
|
|
testing.expect(t, spec_ok)
|
|
_, spec_invalid := checker.spec_index(checker.INVALID_SPEC, 1)
|
|
testing.expect(t, !spec_invalid)
|
|
|
|
testing.expect(t, source.fits_source_length(u64(0xffff_ffff)))
|
|
testing.expect(t, !source.fits_source_length(u64(0x1_0000_0000)))
|
|
}
|
|
|
|
@(test)
|
|
lexer_preserves_newlines_and_skips_comments :: proc(t: ^testing.T) {
|
|
source_file := source.Source{path="test.bro", text="# comment\nmain :: func() void {}\n"}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, stream.items[0].kind, token.Kind.Newline)
|
|
testing.expect_value(t, stream.items[1].kind, token.Kind.Identifier)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_grouped_params_and_multiline_statements :: proc(t: ^testing.T) {
|
|
text := `sum :: func(a,
|
|
b int) int {
|
|
return (a
|
|
+ b)
|
|
}
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(module.functions), 2)
|
|
testing.expect_value(t, len(module.functions[0].params), 2)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_sentinel_many_item_pointer_types :: proc(t: ^testing.T) {
|
|
text := `zero :: func(value [*;0]u8) void {}
|
|
newline :: func(value [*;'\n']mut u8) void {}
|
|
nullable :: func(value ?[*;0]u8) void {}
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
zero, zero_ok := types.node(&module.type_store, module.functions[0].params[0].type)
|
|
newline, newline_ok := types.node(&module.type_store, module.functions[1].params[0].type)
|
|
nullable, nullable_ok := types.node(&module.type_store, module.functions[2].params[0].type)
|
|
nullable_child, nullable_child_ok := types.node(&module.type_store, nullable.child)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, zero_ok && zero.kind == .Pointer && zero.many && zero.has_sentinel && zero.sentinel == 0)
|
|
testing.expect(t, newline_ok && newline.kind == .Pointer && newline.many && newline.mutable &&
|
|
newline.has_sentinel && newline.sentinel == '\n')
|
|
testing.expect(t, nullable_ok && nullable.kind == .Optional)
|
|
testing.expect(t, nullable_child_ok && nullable_child.kind == .Pointer &&
|
|
nullable_child.many && nullable_child.has_sentinel)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_c_function_pointer_types :: proc(t: ^testing.T) {
|
|
text := `take :: c_func(callback ?*c_func(value c_int) c_int) void
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
optional, optional_ok := types.node(&module.type_store, module.functions[0].params[0].type)
|
|
pointer, pointer_ok := types.node(&module.type_store, optional.child)
|
|
function, function_ok := types.node(&module.type_store, pointer.child)
|
|
params := types.params_for(&module.type_store, pointer.child)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, optional_ok && optional.kind == .Optional)
|
|
testing.expect(t, pointer_ok && pointer.kind == .Pointer && pointer.many && !pointer.mutable)
|
|
testing.expect(t, function_ok && function.kind == .Function && function.c_abi && !function.variadic)
|
|
testing.expect(t, function.child == types.C_INT)
|
|
testing.expect_value(t, len(params), 1)
|
|
testing.expect(t, params[0].type == types.C_INT)
|
|
}
|
|
|
|
@(test)
|
|
parser_diagnoses_malformed_sentinel_pointer_types :: proc(t: ^testing.T) {
|
|
text := `bad :: func(value [*0]u8) void {}
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "expected ';' after '*' in sentinel pointer type")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_single_statement_one_line_functions :: proc(t: ^testing.T) {
|
|
text := `give :: func() i8 { return 7 }
|
|
main :: func() void { _ = give() }
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(module.functions), 2)
|
|
testing.expect_value(t, len(module.functions[0].body), 1)
|
|
testing.expect_value(t, len(module.functions[1].body), 1)
|
|
testing.expect_value(t, module.statements[module.functions[0].body[0]].kind, ast.Stmt_Kind.Return)
|
|
testing.expect_value(t, module.statements[module.functions[1].body[0]].kind, ast.Stmt_Kind.Assignment)
|
|
}
|
|
|
|
@(test)
|
|
parser_distinguishes_bodyless_declarations_and_definitions :: proc(t: ^testing.T) {
|
|
text := `foreign :: c_func(value i32) i32
|
|
defined :: c_func(value i32) i32
|
|
{
|
|
return value
|
|
}
|
|
native :: func() i32
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(module.functions), 4)
|
|
testing.expect(t, !module.functions[0].has_body)
|
|
testing.expect(t, module.functions[0].c_abi)
|
|
testing.expect(t, module.functions[1].has_body)
|
|
testing.expect(t, module.functions[1].c_abi)
|
|
testing.expect_value(t, len(module.functions[1].body), 1)
|
|
testing.expect(t, !module.functions[2].has_body)
|
|
testing.expect(t, !module.functions[2].c_abi)
|
|
testing.expect(t, module.functions[3].has_body)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_terminal_c_variadic_markers_and_recovers_nonterminal_markers :: proc(t: ^testing.T) {
|
|
text := `fixed :: c_func(value c_int, ...) c_int
|
|
zero :: c_func(...) void
|
|
bad :: c_func(..., value c_int) c_int
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found_ellipsis := false
|
|
for tok in stream.items {
|
|
found_ellipsis = found_ellipsis || tok.kind == .Ellipsis
|
|
}
|
|
testing.expect(t, found_ellipsis)
|
|
testing.expect(t, module.functions[0].variadic)
|
|
testing.expect_value(t, len(module.functions[0].params), 1)
|
|
testing.expect(t, module.functions[1].variadic)
|
|
testing.expect_value(t, len(module.functions[1].params), 0)
|
|
testing.expect(t, module.functions[2].variadic)
|
|
testing.expect_value(t, len(module.functions[2].params), 1)
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
testing.expect(t, strings.contains(diagnostics.items[0].message, "final parameter"))
|
|
}
|
|
|
|
@(test)
|
|
parser_treats_c_as_contextual_only_before_func :: proc(t: ^testing.T) {
|
|
text := `c :: 5
|
|
x :: c
|
|
foreign :: c_func() i32
|
|
broken :: c 5
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
c_symbol := symbol.intern(&symbols, "c")
|
|
for tok in stream.items {
|
|
if int(tok.span.start) < len(text) && text[int(tok.span.start):int(tok.span.end)] == "c" {
|
|
testing.expect_value(t, tok.kind, token.Kind.Identifier)
|
|
testing.expect_value(t, tok.symbol, c_symbol)
|
|
}
|
|
}
|
|
testing.expect_value(t, len(module.globals), 3)
|
|
testing.expect_value(t, module.exprs[module.globals[1].expr].name, c_symbol)
|
|
testing.expect_value(t, module.exprs[module.globals[2].expr].name, c_symbol)
|
|
testing.expect(t, module.functions[0].c_abi)
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
testing.expect(t, strings.contains(diagnostics.items[0].message, "followed by a newline"))
|
|
}
|
|
|
|
@(test)
|
|
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)
|
|
}
|
|
|
|
nested_expression_source :: proc(call: bool, depth: int) -> string {
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
if call {
|
|
strings.write_string(&builder, "identity :: func(value i32) i32 { return value }\nvalue :: ")
|
|
for _ in 0..<depth {
|
|
strings.write_string(&builder, "identity(")
|
|
}
|
|
} else {
|
|
strings.write_string(&builder, "value :: ")
|
|
for _ in 0..<depth {
|
|
strings.write_byte(&builder, '(')
|
|
}
|
|
}
|
|
strings.write_byte(&builder, '1')
|
|
for _ in 0..<depth {
|
|
strings.write_byte(&builder, ')')
|
|
}
|
|
strings.write_string(&builder, "\nmain :: func() void {}\n")
|
|
return strings.clone(strings.to_string(builder))
|
|
}
|
|
|
|
nested_negation_source :: proc(depth: int) -> string {
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
strings.write_string(&builder, "value :: ")
|
|
for _ in 0..<depth {
|
|
strings.write_byte(&builder, '-')
|
|
}
|
|
strings.write_string(&builder, "1\nmain :: func() void {}\n")
|
|
return strings.clone(strings.to_string(builder))
|
|
}
|
|
|
|
parse_nesting_result :: proc(text: string) -> (count: int, found_budget: bool) {
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
for diagnostic in diagnostics.items {
|
|
found_budget = found_budget || strings.contains(diagnostic.message, "expression nesting exceeds 256 levels")
|
|
}
|
|
return len(diagnostics.items), found_budget
|
|
}
|
|
|
|
@(test)
|
|
parser_enforces_explicit_expression_nesting_budget :: proc(t: ^testing.T) {
|
|
modes := [?]bool{false, true}
|
|
for call in modes {
|
|
at_limit := nested_expression_source(call, parser.MAX_EXPRESSION_NESTING)
|
|
defer delete(at_limit)
|
|
count, found := parse_nesting_result(at_limit)
|
|
testing.expect_value(t, count, 0)
|
|
testing.expect(t, !found)
|
|
|
|
over_limit := nested_expression_source(call, parser.MAX_EXPRESSION_NESTING+1)
|
|
defer delete(over_limit)
|
|
_, found = parse_nesting_result(over_limit)
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
at_limit := nested_negation_source(parser.MAX_EXPRESSION_NESTING)
|
|
defer delete(at_limit)
|
|
count, found := parse_nesting_result(at_limit)
|
|
testing.expect_value(t, count, 0)
|
|
testing.expect(t, !found)
|
|
|
|
over_limit := nested_negation_source(parser.MAX_EXPRESSION_NESTING+1)
|
|
defer delete(over_limit)
|
|
_, found = parse_nesting_result(over_limit)
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T) {
|
|
options, valid := parse_cli_args([]string{
|
|
"brolang",
|
|
"app",
|
|
"--c-link",
|
|
"native.c",
|
|
"-o",
|
|
"app.out",
|
|
"--c-library-path",
|
|
"vendor/lib",
|
|
"--c-library",
|
|
"thing",
|
|
"--c-link",
|
|
"helper.o",
|
|
"--c-include-path",
|
|
"vendor/include",
|
|
"--c-define",
|
|
"FEATURE=1",
|
|
"--target",
|
|
"aarch64-macos",
|
|
})
|
|
defer delete(options.link_arguments)
|
|
defer delete(options.c_options.include_paths)
|
|
defer delete(options.c_options.defines)
|
|
|
|
testing.expect(t, valid)
|
|
testing.expect_value(t, options.input_path, "app")
|
|
testing.expect_value(t, options.output_path, "app.out")
|
|
testing.expect_value(t, len(options.link_arguments), 4)
|
|
testing.expect_value(t, options.link_arguments[0].kind, linker.Kind.Input)
|
|
testing.expect_value(t, options.link_arguments[0].value, "native.c")
|
|
testing.expect_value(t, options.link_arguments[1].kind, linker.Kind.Library_Path)
|
|
testing.expect_value(t, options.link_arguments[2].kind, linker.Kind.Library)
|
|
testing.expect_value(t, options.link_arguments[3].value, "helper.o")
|
|
testing.expect_value(t, options.c_options.include_paths[0], "vendor/include")
|
|
testing.expect_value(t, options.c_options.defines[0], "FEATURE=1")
|
|
testing.expect_value(t, target.name(options.target), "aarch64-macos")
|
|
|
|
_, unknown_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--unknown", "value"})
|
|
_, incomplete_valid := parse_cli_args([]string{"brolang", "app", "-o"})
|
|
_, duplicate_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "one", "-o", "two"})
|
|
_, duplicate_empty_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "", "-o", "two"})
|
|
_, invalid_target := parse_cli_args([]string{"brolang", "app", "-o", "out", "--target", "x86_64-linux"})
|
|
_, legacy_link := parse_cli_args([]string{"brolang", "app", "-o", "out", "--link", "native.c"})
|
|
_, legacy_library_path := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library-path", "vendor/lib"})
|
|
_, legacy_library := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library", "thing"})
|
|
testing.expect(t, !unknown_valid)
|
|
testing.expect(t, !incomplete_valid)
|
|
testing.expect(t, !duplicate_output_valid)
|
|
testing.expect(t, !duplicate_empty_output_valid)
|
|
testing.expect(t, !invalid_target)
|
|
testing.expect(t, !legacy_link)
|
|
testing.expect(t, !legacy_library_path)
|
|
testing.expect(t, !legacy_library)
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_bare_and_aliased_imports :: proc(t: ^testing.T) {
|
|
text := `import
|
|
"../math"
|
|
other :: import "../math"
|
|
escaped :: import "dir\"name\\tail"
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(module.imports), 3)
|
|
testing.expect_value(t, symbol.resolve(&symbols, module.imports[0].alias), "")
|
|
testing.expect_value(t, module.imports[0].path, "../math")
|
|
testing.expect_value(t, symbol.resolve(&symbols, module.imports[1].alias), "other")
|
|
testing.expect_value(t, module.imports[2].path, "dir\"name\\tail")
|
|
}
|
|
|
|
@(test)
|
|
parser_accepts_chained_field_access :: proc(t: ^testing.T) {
|
|
text := `main :: func() void {
|
|
_ = first.second.value
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
lexer_diagnoses_invalid_import_strings :: proc(t: ^testing.T) {
|
|
text := "import \"bad\\q\"\nimport \"unterminated\n"
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 2)
|
|
}
|
|
|
|
@(test)
|
|
package_loader_discovers_lexical_immediate_bro_files :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
module, loaded := loader.load("examples/packages/basic/app", &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(module.packages), 2)
|
|
testing.expect_value(t, len(module.files), 3)
|
|
testing.expect(t, strings.has_suffix(sources.items[module.files[0].source].path, "/main.bro"))
|
|
testing.expect(t, strings.has_suffix(sources.items[module.files[1].source].path, "/value.bro"))
|
|
testing.expect(t, strings.has_suffix(sources.items[module.files[2].source].path, "/math.bro"))
|
|
}
|
|
|
|
@(test)
|
|
multi_source_diagnostics_report_the_originating_file :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
module, loaded := loader.load("examples/packages/file_local/app", &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&module)
|
|
hir_module := checker.check(&module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect(t, loaded)
|
|
found := false
|
|
for _, diagnostic_index in diagnostics.items {
|
|
message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index))
|
|
if strings.contains(message, "/b.bro:2:9:") &&
|
|
strings.contains(message, "unknown package alias 'math'") {
|
|
found = true
|
|
}
|
|
delete(message)
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
semantic_lookup_diagnoses_wrong_declaration_kinds :: proc(t: ^testing.T) {
|
|
text := `value :: 1
|
|
give :: func() i8 {
|
|
return 1
|
|
}
|
|
main :: func() void {
|
|
_ = value()
|
|
_ = give
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_global := false
|
|
found_function := false
|
|
for diagnostic in diagnostics.items {
|
|
found_global = found_global || strings.contains(diagnostic.message, "'value' is a global, not a function")
|
|
found_function = found_function || strings.contains(diagnostic.message, "'give' is a function, not a global value")
|
|
}
|
|
testing.expect(t, found_global)
|
|
testing.expect(t, found_function)
|
|
}
|
|
|
|
@(test)
|
|
pipeline_emits_specialized_calling_conventions_and_checked_add :: proc(t: ^testing.T) {
|
|
text := `sum_c :: c_func(a, b int) int {
|
|
return a + b
|
|
}
|
|
sum_bro :: func(a, b int) int {
|
|
return a + b
|
|
}
|
|
main :: func() void {
|
|
_ = sum_c(1, 2)
|
|
_ = sum_bro(1, 2)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
second_llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(second_llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, llvm_text, second_llvm_text)
|
|
testing.expect(t, strings.contains(llvm_text, "define signext i8 @bro_c__p0__sum_c__i8__i8"))
|
|
testing.expect(t, strings.contains(llvm_text, "define internal fastcc i8 @bro__p0__sum_bro__i8__i8"))
|
|
testing.expect(t, strings.contains(llvm_text, "@llvm.sadd.with.overflow.i8"))
|
|
}
|
|
|
|
@(test)
|
|
pipeline_emits_only_referenced_foreign_declarations_with_exact_names :: proc(t: ^testing.T) {
|
|
text := `used :: c_func(a, b i32) i32
|
|
unused :: c_func() i32
|
|
bodyful :: c_func(value i32) i32 {
|
|
return value
|
|
}
|
|
main :: func() void {
|
|
_ = used(1, 2)
|
|
_ = bodyful(3)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @used(i32, i32)"))
|
|
testing.expect(t, strings.contains(llvm_text, "call i32 @used(i32 1, i32 2)"))
|
|
testing.expect(t, !strings.contains(llvm_text, "@unused("))
|
|
testing.expect(t, strings.contains(llvm_text, "define i32 @bro_c__p0__bodyful__i32"))
|
|
}
|
|
|
|
@(test)
|
|
c_primitives_remain_distinct_with_apple_silicon_representations :: proc(t: ^testing.T) {
|
|
testing.expect(t, types.C_CHAR != types.C_SCHAR)
|
|
testing.expect(t, types.C_SCHAR != types.C_UCHAR)
|
|
testing.expect(t, types.C_INT != types.I32)
|
|
testing.expect(t, types.C_ULONG != types.U64)
|
|
testing.expect_value(t, types.representation(types.C_CHAR), types.I8)
|
|
testing.expect_value(t, types.representation(types.C_SCHAR), types.I8)
|
|
testing.expect_value(t, types.representation(types.C_UCHAR), types.U8)
|
|
testing.expect_value(t, types.representation(types.C_SHORT), types.I16)
|
|
testing.expect_value(t, types.representation(types.C_USHORT), types.U16)
|
|
testing.expect_value(t, types.representation(types.C_INT), types.I32)
|
|
testing.expect_value(t, types.representation(types.C_UINT), types.U32)
|
|
testing.expect_value(t, types.representation(types.C_LONG), types.I64)
|
|
testing.expect_value(t, types.representation(types.C_ULONG), types.U64)
|
|
testing.expect_value(t, types.representation(types.C_LONGLONG), types.I64)
|
|
testing.expect_value(t, types.representation(types.C_ULONGLONG), types.U64)
|
|
testing.expect_value(t, types.representation(types.C_FLOAT), types.F32)
|
|
testing.expect_value(t, types.representation(types.C_DOUBLE), types.F64)
|
|
testing.expect_value(t, types.representation(types.C_LONGDOUBLE), types.F64)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.I8), types.C_INT)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.U16), types.C_INT)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.C_CHAR), types.C_INT)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.C_USHORT), types.C_INT)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.F32), types.C_DOUBLE)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.C_FLOAT), types.C_DOUBLE)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.U32), types.U32)
|
|
testing.expect_value(t, types.c_vararg_promotion(types.C_DOUBLE), types.C_DOUBLE)
|
|
testing.expect_value(t, target.llvm_triple(target.DEFAULT), "arm64-apple-macosx13.0.0")
|
|
}
|
|
|
|
@(test)
|
|
interop_foundation_emits_compounds_and_narrow_c_abi_attributes :: proc(t: ^testing.T) {
|
|
text := `Point :: struct {
|
|
x i32
|
|
y i32
|
|
}
|
|
signed :: c_func(value c_char) c_char
|
|
unsigned :: c_func(value c_uchar) c_uchar
|
|
exact :: c_func(value u32) u32
|
|
fallback :: func() i32 {
|
|
return 9
|
|
}
|
|
main :: func() void {
|
|
c :: 1
|
|
values [2;0]mut u8 = [1, 2]
|
|
point Point :: Point{x = 3, y = 4}
|
|
maybe ?i32 = 5
|
|
_ = c
|
|
_ = values[2]
|
|
_ = (&values).ptr + 1
|
|
_ = values.len
|
|
_ = values[0..2]
|
|
_ = "hello".ptr
|
|
_ = "hello".len
|
|
_ = point.x
|
|
_ = maybe?
|
|
_ = maybe orelse 0
|
|
_ = maybe orelse fallback()
|
|
_ = signed(1)
|
|
_ = unsigned(1)
|
|
_ = exact(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "target triple = \"arm64-apple-macosx13.0.0\""))
|
|
testing.expect(t, strings.contains(llvm_text, "declare signext i8 @signed(i8 signext)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare zeroext i8 @unsigned(i8 zeroext)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @exact(i32)"))
|
|
testing.expect(t, strings.contains(llvm_text, "@bro.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\""))
|
|
testing.expect(t, strings.contains(llvm_text, "getelementptr [3 x i8]"))
|
|
testing.expect(t, strings.contains(llvm_text, "attempted to unwrap none"))
|
|
testing.expect(t, strings.contains(llvm_text, "orelse_fallback"))
|
|
testing.expect(t, strings.contains(llvm_text, "orelse_some"))
|
|
}
|
|
|
|
@(test)
|
|
string_literals_preserve_static_length_and_sentinel_through_pointer_views :: proc(t: ^testing.T) {
|
|
text := `take_sentinel_pointer :: func(value [*;0]u8) void {}
|
|
take_mut_sentinel_pointer :: func(value [*;0]mut u8) void {}
|
|
take_pointer :: func(value *u8) void {}
|
|
take_sentinel_slice :: func(value [;0]u8) void {}
|
|
take_mut_sentinel_slice :: func(value [;0]mut u8) void {}
|
|
take_slice :: func(value []u8) void {}
|
|
take_c_string :: c_func(value *c_char) c_int
|
|
take_c_sentinel :: c_func(value [*;0]c_char) c_int
|
|
main :: func() void {
|
|
text :: "hello"
|
|
values [2;0]mut u8 = [1, 2]
|
|
pointer :: &values
|
|
_ = text.len
|
|
_ = text.ptr
|
|
_ = text[0]
|
|
_ = text[1..]
|
|
_ = pointer.len
|
|
_ = pointer.ptr
|
|
_ = pointer[0]
|
|
_ = pointer[1..]
|
|
offset [*;0]u8 :: text.ptr + 1
|
|
suffix [*;0]u8 :: text[1..].ptr
|
|
middle []u8 :: text[1..3]
|
|
_ = offset
|
|
_ = suffix
|
|
_ = middle
|
|
take_sentinel_pointer(text)
|
|
take_pointer(text)
|
|
take_sentinel_slice(text)
|
|
take_slice(text)
|
|
take_mut_sentinel_pointer(pointer)
|
|
take_mut_sentinel_slice(pointer)
|
|
take_sentinel_pointer(pointer)
|
|
take_sentinel_slice(pointer)
|
|
_ = take_c_string(text)
|
|
_ = take_c_sentinel(text)
|
|
_ = take_c_string(text.ptr)
|
|
_ = take_c_sentinel(text.ptr)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
string_type := types.INVALID
|
|
decays := 0
|
|
for expr in hir_module.exprs {
|
|
if expr.kind == .String {
|
|
string_type = expr.type
|
|
}
|
|
if expr.kind == .Decay_Array_Pointer {
|
|
decays += 1
|
|
}
|
|
}
|
|
pointer, array, string_ok := types.array_pointer(string_type, &hir_module.types)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, string_ok && !pointer.mutable && array.child == types.U8 &&
|
|
array.count == 5 && array.has_sentinel && array.sentinel == 0)
|
|
testing.expect(t, decays >= 6)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro.str.0 = private unnamed_addr constant [6 x i8] c\"hello\\00\""))
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_string(ptr)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare i32 @take_c_sentinel(ptr)"))
|
|
}
|
|
|
|
@(test)
|
|
array_pointer_and_c_string_coercion_restrictions_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := `take_c_string :: c_func(value *c_char) c_int
|
|
take_mut_c_string :: c_func(value *mut c_char) c_int
|
|
take_pointer :: func(value *u8) void {}
|
|
take_mut_pointer :: func(value *mut u8) void {}
|
|
take_slice :: func(value []u8) void {}
|
|
bad_sentinel :: func(value [*;256]u8) void {}
|
|
main :: func() void {
|
|
values [1;0]mut u8 = [1]
|
|
_ = values.ptr
|
|
take_pointer(values)
|
|
take_slice(values)
|
|
ordinary *u8 :: "hello"
|
|
nonzero [1;'\n']mut u8 = [1]
|
|
take_pointer("hello"[1..])
|
|
_ = take_c_string(ordinary)
|
|
_ = take_c_string((&nonzero).ptr)
|
|
_ = take_c_string(1)
|
|
take_mut_pointer("hello")
|
|
_ = take_mut_c_string("hello")
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
conversion_errors := 0
|
|
array_ptr_error := false
|
|
sentinel_error := false
|
|
for diagnostic in diagnostics.items {
|
|
conversion_errors += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0
|
|
array_ptr_error = array_ptr_error ||
|
|
strings.contains(diagnostic.message, "arrays do not expose '.ptr'")
|
|
sentinel_error = sentinel_error ||
|
|
strings.contains(diagnostic.message, "sentinel value does not fit array, slice, or pointer")
|
|
}
|
|
testing.expect_value(t, conversion_errors, 8)
|
|
testing.expect(t, array_ptr_error)
|
|
testing.expect(t, sentinel_error)
|
|
}
|
|
|
|
@(test)
|
|
immutable_pointer_and_slice_bindings_preserve_mutable_pointees :: proc(t: ^testing.T) {
|
|
text := `main :: func() void {
|
|
values [2]mut u8 = [1, 2]
|
|
pointer *mut u8 :: (&values).ptr
|
|
slice []mut u8 :: values[0..]
|
|
pointer[0] = 3
|
|
slice[1] = 4
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
}
|
|
|
|
@(test)
|
|
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_may_be_opaque :: proc(t: ^testing.T) {
|
|
text := `Defined :: c_struct {
|
|
value c_int
|
|
}
|
|
Opaque :: c_struct
|
|
Empty :: c_struct {}
|
|
Bad :: c_struct {
|
|
values []i32
|
|
}
|
|
read :: c_func(value @Defined) c_int
|
|
pass :: c_func(value Defined) Defined
|
|
bad_opaque :: c_func(value Opaque) void
|
|
main :: func() void {
|
|
_ = pass(Defined { value = 1 })
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_opaque := false
|
|
found_bad_layout := false
|
|
found_empty := false
|
|
for diagnostic in diagnostics.items {
|
|
found_opaque = found_opaque || strings.contains(diagnostic.message, "cannot be passed by value")
|
|
found_bad_layout = found_bad_layout || strings.contains(diagnostic.message, "C-layout-compatible")
|
|
found_empty = found_empty || strings.contains(diagnostic.message, "at least one field")
|
|
}
|
|
testing.expect(t, found_opaque)
|
|
testing.expect(t, found_bad_layout)
|
|
testing.expect(t, found_empty)
|
|
}
|
|
|
|
@(test)
|
|
aarch64_c_record_abi_classifies_fixed_parameters_and_results :: proc(t: ^testing.T) {
|
|
text := `Small :: c_struct {
|
|
left c_int
|
|
right c_int
|
|
}
|
|
Medium :: c_struct {
|
|
first c_int
|
|
second c_int
|
|
third c_int
|
|
}
|
|
Hfa :: c_struct {
|
|
x c_float
|
|
y c_float
|
|
}
|
|
Large :: c_struct {
|
|
first c_long
|
|
second c_long
|
|
third c_long
|
|
}
|
|
small :: c_func(value Small) Small
|
|
medium :: c_func(value Medium) Medium
|
|
hfa :: c_func(value Hfa) Hfa
|
|
large :: c_func(value Large) Large
|
|
main :: func() void {
|
|
_ = small(Small { left = 1, right = 2 })
|
|
_ = medium(Medium { first = 1, second = 2, third = 3 })
|
|
_ = hfa(Hfa { x = 1.0, y = 2.0 })
|
|
_ = large(Large { first = 1, second = 2, third = 3 })
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "declare i64 @small(i64)"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare [2 x i64] @medium([2 x i64])"))
|
|
testing.expect(t, strings.contains(llvm_text, "@hfa([2 x float])"))
|
|
testing.expect(t, strings.contains(llvm_text, "declare void @large(ptr sret("))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64"))
|
|
}
|
|
|
|
@(test)
|
|
invalid_foreign_declarations_are_eagerly_diagnosed_and_calls_trap :: proc(t: ^testing.T) {
|
|
text := `bad :: c_func(value int) int
|
|
native :: func() i32
|
|
main :: func() void {
|
|
_ = bad(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_parameter := false
|
|
found_result := false
|
|
found_native := false
|
|
for diagnostic in diagnostics.items {
|
|
found_parameter = found_parameter || strings.contains(diagnostic.message, "requires concrete parameter types")
|
|
found_result = found_result || strings.contains(diagnostic.message, "requires a concrete or void result type")
|
|
found_native = found_native || strings.contains(diagnostic.message, "must use 'c_func'")
|
|
}
|
|
testing.expect(t, found_parameter)
|
|
testing.expect(t, found_result)
|
|
testing.expect(t, found_native)
|
|
testing.expect(t, !strings.contains(llvm_text, "@bad("))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
|
|
}
|
|
|
|
@(test)
|
|
duplicate_foreign_symbols_across_packages_are_poisoned :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load("examples/packages/foreign_duplicate/app", &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
duplicate_count := 0
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "duplicate foreign symbol 'same'") {
|
|
duplicate_count += 1
|
|
}
|
|
}
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, duplicate_count, 2)
|
|
testing.expect(t, !strings.contains(llvm_text, "@same("))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
|
|
}
|
|
|
|
@(test)
|
|
bodyless_root_main_recovers_as_a_trap_definition :: proc(t: ^testing.T) {
|
|
text := "main :: c_func() i32\n"
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "main must have a body")
|
|
}
|
|
testing.expect(t, found)
|
|
testing.expect_value(t, len(ir_module.functions), 1)
|
|
testing.expect_value(t, ir_module.functions[0].implementation, ir.Implementation.Definition)
|
|
testing.expect(t, strings.contains(llvm_text, "define i32 @main()"))
|
|
testing.expect(t, !strings.contains(llvm_text, "declare i32 @main()"))
|
|
}
|
|
|
|
@(test)
|
|
literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) {
|
|
text := `return_i16 :: func() i16 {
|
|
return 1 + 2
|
|
}
|
|
take_i16 :: func(value i16) i16 {
|
|
return value
|
|
}
|
|
take_int :: func(value int) int {
|
|
return value
|
|
}
|
|
main :: func() void {
|
|
local i16 :: 1 + 2
|
|
_ = 100 + (20 + 8)
|
|
_ = return_i16()
|
|
_ = take_i16(1 + 2)
|
|
_ = take_int(127 + 1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__take_int__i16"))
|
|
for function in ir_module.functions {
|
|
for instruction in function.instructions {
|
|
testing.expect(t, instruction.op != ir.Opcode.Add_Checked)
|
|
}
|
|
}
|
|
|
|
for function in hir_module.functions {
|
|
for statement_id in function.body {
|
|
statement := hir_module.statements[statement_id]
|
|
if statement.expr < 0 {
|
|
continue
|
|
}
|
|
expr := hir_module.exprs[statement.expr]
|
|
if expr.kind == .Integer {
|
|
testing.expect(t, types.equal(expr.type, types.I16))
|
|
}
|
|
if expr.kind == .Call && symbol.resolve(&symbols, function.name) == "main" && len(expr.args) > 0 {
|
|
arg := hir_module.exprs[expr.args[0]]
|
|
testing.expect_value(t, arg.kind, hir.Expr_Kind.Integer)
|
|
testing.expect(t, types.equal(arg.type, types.I16))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
runtime_arithmetic_does_not_inherit_result_context :: proc(t: ^testing.T) {
|
|
text := `widen_after_add :: func(value i8) i16 {
|
|
return value + 1
|
|
}
|
|
main :: func() void {
|
|
_ = widen_after_add(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
found := false
|
|
for function in hir_module.functions {
|
|
if symbol.resolve(&symbols, function.name) != "widen_after_add" {
|
|
continue
|
|
}
|
|
found = true
|
|
statement := hir_module.statements[function.body[0]]
|
|
widen := hir_module.exprs[statement.expr]
|
|
add := hir_module.exprs[widen.left]
|
|
testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen)
|
|
testing.expect(t, types.equal(widen.type, types.I16))
|
|
testing.expect_value(t, add.kind, hir.Expr_Kind.Add)
|
|
testing.expect(t, types.equal(add.type, types.I8))
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
parser_recovers_after_invalid_tokens :: proc(t: ^testing.T) {
|
|
text := `broken @ declaration
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect(t, len(diagnostics.items) > 0)
|
|
testing.expect_value(t, len(module.functions), 1)
|
|
testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "main")
|
|
}
|
|
|
|
@(test)
|
|
return_sink_and_unconsumed_values_have_distinct_hir :: proc(t: ^testing.T) {
|
|
text := `give :: func() i8 {
|
|
return 1
|
|
}
|
|
done :: func() void {
|
|
return _
|
|
}
|
|
main :: func() void {
|
|
done()
|
|
_ = give()
|
|
give()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
done_id, main_id := -1, -1
|
|
for function, id in hir_module.functions {
|
|
if symbol.resolve(&symbols, function.name) == "done" {
|
|
done_id = id
|
|
} else if symbol.resolve(&symbols, function.name) == "main" {
|
|
main_id = id
|
|
}
|
|
}
|
|
testing.expect(t, done_id >= 0)
|
|
testing.expect(t, main_id >= 0)
|
|
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].kind, hir.Stmt_Kind.Return)
|
|
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].expr, hir.INVALID_EXPR)
|
|
main := hir_module.functions[main_id]
|
|
testing.expect_value(t, hir_module.statements[main.body[0]].kind, hir.Stmt_Kind.Expression)
|
|
testing.expect_value(t, hir_module.statements[main.body[1]].kind, hir.Stmt_Kind.Sink)
|
|
testing.expect_value(t, hir_module.statements[main.body[2]].kind, hir.Stmt_Kind.Trap)
|
|
}
|
|
|
|
@(test)
|
|
recursive_specialization_reaches_a_fixed_point :: proc(t: ^testing.T) {
|
|
text := `a :: func(value int) i32 {
|
|
return b(value)
|
|
}
|
|
b :: func(value int) i32 {
|
|
return a(value)
|
|
}
|
|
main :: func() void {
|
|
_ = a(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 3)
|
|
}
|
|
|
|
@(test)
|
|
stale_specializations_are_pruned_after_inference :: proc(t: ^testing.T) {
|
|
text := `derived :: identity(make())
|
|
wide :: delayed()
|
|
identity :: func(value int) int {
|
|
return value
|
|
}
|
|
make :: func() int {
|
|
return wide
|
|
return 1
|
|
}
|
|
delayed :: func() int {
|
|
return 128
|
|
}
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 4)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__identity__i16"))
|
|
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity__i8"))
|
|
}
|
|
|
|
@(test)
|
|
eager_global_calls_root_specializations :: proc(t: ^testing.T) {
|
|
text := `make :: func() i32 {
|
|
return 7
|
|
}
|
|
unused_native :: func() i32 {
|
|
return 9
|
|
}
|
|
unused_foreign :: c_func() i32
|
|
value i32 :: make()
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 2)
|
|
testing.expect(t, strings.contains(llvm_text, "@bro__p0__make("))
|
|
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__unused_native("))
|
|
testing.expect(t, !strings.contains(llvm_text, "@unused_foreign("))
|
|
}
|
|
|
|
@(test)
|
|
malformed_generic_calls_do_not_retain_specializations :: proc(t: ^testing.T) {
|
|
text := `identity :: func(value int) int {
|
|
return value
|
|
}
|
|
bad :: identity(1, 2)
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
found_arity := false
|
|
for diagnostic in diagnostics.items {
|
|
found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 arguments, got 2")
|
|
}
|
|
testing.expect(t, found_arity)
|
|
testing.expect_value(t, len(hir_module.functions), 1)
|
|
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity"))
|
|
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
|
|
}
|
|
|
|
@(test)
|
|
long_generic_call_chain_reaches_a_fixed_point :: proc(t: ^testing.T) {
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
for index in 0 ..< 70 {
|
|
fmt.sbprintf(&builder, "fn%d :: func(value int) int ", index)
|
|
strings.write_string(&builder, "{ return ")
|
|
if index == 69 {
|
|
strings.write_string(&builder, "value")
|
|
} else {
|
|
fmt.sbprintf(&builder, "fn%d(value)", index+1)
|
|
}
|
|
strings.write_string(&builder, " }\n")
|
|
}
|
|
strings.write_string(&builder, "main :: func() i32 { return fn0(1) }\n")
|
|
|
|
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 71)
|
|
}
|
|
|
|
@(test)
|
|
globals_and_generic_results_reach_a_shared_fixed_point :: proc(t: ^testing.T) {
|
|
text := `derived :: identity(base)
|
|
base :: make()
|
|
identity :: func(value int) int {
|
|
return value
|
|
}
|
|
make :: func() int {
|
|
return 1
|
|
}
|
|
main :: func() i32 {
|
|
return derived
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, len(hir_module.functions), 3)
|
|
for global in hir_module.globals {
|
|
testing.expect(t, types.equal(global.type, types.I8))
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) {
|
|
text := `broken :: func(value, value i8, nope void) void {}
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_duplicate := false
|
|
found_void := false
|
|
for diagnostic in diagnostics.items {
|
|
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate parameter 'value'")
|
|
found_void = found_void || strings.contains(diagnostic.message, "void is only valid as a function result type")
|
|
}
|
|
testing.expect(t, found_duplicate)
|
|
testing.expect(t, found_void)
|
|
}
|
|
|
|
run_command_success :: proc(command: []string) -> bool {
|
|
state, stdout, stderr, err := os2.process_exec(
|
|
os2.Process_Desc{command=command},
|
|
context.allocator,
|
|
)
|
|
delete(stdout)
|
|
delete(stderr)
|
|
return err == nil && state.exit_code == 0
|
|
}
|
|
|
|
prepare_native_object :: proc(output: string) -> bool {
|
|
local_cache := fmt.aprintf("ZIG_LOCAL_CACHE_DIR=%s-zig-cache", output)
|
|
defer delete(local_cache)
|
|
global_cache := fmt.aprintf("ZIG_GLOBAL_CACHE_DIR=%s-zig-global-cache", output)
|
|
defer delete(global_cache)
|
|
return run_command_success([]string{
|
|
"/usr/bin/env",
|
|
local_cache,
|
|
global_cache,
|
|
"zig",
|
|
"cc",
|
|
"-c",
|
|
"examples/interop/manual/native.c",
|
|
"-o",
|
|
output,
|
|
})
|
|
}
|
|
|
|
prepare_native_archive :: proc(object, archive: string) -> bool {
|
|
return run_command_success([]string{
|
|
"/usr/bin/ar",
|
|
"-rcs",
|
|
archive,
|
|
object,
|
|
})
|
|
}
|
|
|
|
run_executable :: proc(path: string) -> os2.Process_State {
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{path}},
|
|
context.allocator,
|
|
)
|
|
delete(stdout)
|
|
delete(stderr)
|
|
return state
|
|
}
|
|
|
|
@(test)
|
|
valid_program_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-valid"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/prototype", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
c_printf_accepts_a_string_literal :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-printf"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/interop/printf", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
sentinel_pointer_views_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-sentinel-pointer"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/sentinel_pointer", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 303)
|
|
}
|
|
|
|
@(test)
|
|
control_flow_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-control-flow"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/control_flow", output)
|
|
testing.expect_value(t, status, 0)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}},
|
|
context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
// if / else if / else, comparisons, logical and/or/not, bool locals, and
|
|
// block scoping together produce 42.
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
// Short-circuit: `noisy()` is never reached, so its output must be absent,
|
|
// while the taken or-branch must print.
|
|
testing.expect(t, !strings.contains(string(stdout), "rhs-evaluated"))
|
|
testing.expect(t, strings.contains(string(stdout), "or-taken"))
|
|
}
|
|
|
|
@(test)
|
|
foreign_function_links_from_c_source :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-foreign-source"
|
|
defer _ = os.remove(output)
|
|
arguments := []linker.Argument{{kind=.Input, value="examples/interop/manual/native.c"}}
|
|
status := compiler_core.compile_package("examples/interop/manual", output, arguments)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
restricted_c_header_imports_compile_and_link :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-header-import"
|
|
defer _ = os.remove(output)
|
|
arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}}
|
|
c_options := cimport.Options{
|
|
include_paths=[]string{"examples/interop/header/include"},
|
|
defines=[]string{"BROLANG_FEATURE"},
|
|
}
|
|
status := compiler_core.compile_package("examples/interop/header/app", output, arguments, target.DEFAULT, c_options)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
by_value_c_records_and_unions_compile_and_link :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-records"
|
|
defer _ = os.remove(output)
|
|
arguments := []linker.Argument{{kind=.Input, value="examples/interop/records/native.c"}}
|
|
c_options := cimport.Options{include_paths=[]string{"examples/interop/records/include"}}
|
|
status := compiler_core.compile_package("examples/interop/records/app", output, arguments, target.DEFAULT, c_options)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 1)
|
|
}
|
|
|
|
@(test)
|
|
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)
|
|
unused_invalid_global_does_not_trap :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-invalid-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/invalid_unused_global", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
used_invalid_global_traps :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-invalid-used"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/invalid_used_global", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
main_int_is_constrained_to_i32 :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-main-int"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/main_int", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 3)
|
|
}
|
|
|
|
@(test)
|
|
main_i32_returns_directly :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-main-i32"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/main_i32", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 4)
|
|
}
|
|
|
|
@(test)
|
|
transitive_problematic_global_is_deferred :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-transitive-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/invalid_transitive_unused_global", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
checked_addition_traps_on_overflow :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-overflow"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/overflow", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
checked_runtime_negation_traps_for_every_signed_width :: proc(t: ^testing.T) {
|
|
Case :: struct {
|
|
type_name: string,
|
|
magnitude: string,
|
|
}
|
|
cases := [?]Case{
|
|
{type_name="i8", magnitude="128"},
|
|
{type_name="i16", magnitude="32768"},
|
|
{type_name="i32", magnitude="2147483648"},
|
|
{type_name="i64", magnitude="9223372036854775808"},
|
|
}
|
|
for test_case in cases {
|
|
directory := fmt.aprintf("/tmp/brolang-test-negate-overflow-%s", test_case.type_name)
|
|
main_path := fmt.aprintf("%s/main.bro", directory)
|
|
output := fmt.aprintf("/tmp/brolang-test-negate-overflow-output-%s", test_case.type_name)
|
|
builder := strings.builder_make()
|
|
fmt.sbprintf(
|
|
&builder,
|
|
"negate :: func(value %s) %s {{ return -value }}\nmain :: func() void {{ _ = negate(-%s) }}\n",
|
|
test_case.type_name,
|
|
test_case.type_name,
|
|
test_case.magnitude,
|
|
)
|
|
text := strings.clone(strings.to_string(builder))
|
|
strings.builder_destroy(&builder)
|
|
_ = os2.remove_all(directory)
|
|
_ = os.remove(output)
|
|
testing.expect(t, os.make_directory(directory) == nil)
|
|
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
|
|
status := compiler_core.compile_package(directory, output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
_ = os.remove(output)
|
|
_ = os2.remove_all(directory)
|
|
delete(text)
|
|
delete(output)
|
|
delete(main_path)
|
|
delete(directory)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
constant_that_does_not_fit_context_produces_trap_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-constant-context-error"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/constant_context_error", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
constant_beyond_i64_produces_trap_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-constant-i64-overflow"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/constant_i64_overflow", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
same_line_statements_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := "main :: func() void { _ = 1 _ = 2 }\n"
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect(t, len(diagnostics.items) > 0)
|
|
}
|
|
|
|
@(test)
|
|
missing_main_produces_trap_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-missing-main"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/missing_main", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
backend_failure_preserves_existing_output :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-preserved-output"
|
|
defer _ = os.remove(output)
|
|
previous := "previous artifact"
|
|
testing.expect(t, os.write_entire_file(output, transmute([]byte)previous))
|
|
testing.expect(t, !backend.compile("/definitely/not/llvm.ll", output))
|
|
data, ok := os.read_entire_file(output)
|
|
defer delete(data)
|
|
testing.expect(t, ok)
|
|
testing.expect_value(t, string(data), previous)
|
|
}
|
|
|
|
@(test)
|
|
backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T) {
|
|
arguments := []linker.Argument{
|
|
{kind=.Input, value="native.c"},
|
|
{kind=.Library_Path, value="vendor/lib"},
|
|
{kind=.Library, value="thing"},
|
|
{kind=.Input, value="helper.o"},
|
|
}
|
|
c_options := cimport.Options{
|
|
include_paths=[]string{"vendor/include"},
|
|
defines=[]string{"FEATURE=1"},
|
|
}
|
|
command := backend.build_command("module.ll", "program", arguments, target.DEFAULT, c_options)
|
|
defer backend.destroy_command(command)
|
|
|
|
expected := []string{
|
|
"/usr/bin/env",
|
|
"zig",
|
|
"cc",
|
|
"-target",
|
|
"aarch64-macos",
|
|
"-Wno-override-module",
|
|
"-Wno-unused-command-line-argument",
|
|
"module.ll",
|
|
"-Ivendor/include",
|
|
"-DFEATURE=1",
|
|
"native.c",
|
|
"-Lvendor/lib",
|
|
"-lthing",
|
|
"helper.o",
|
|
"-o",
|
|
"program",
|
|
}
|
|
testing.expect_value(t, len(command), len(expected))
|
|
for value, index in expected {
|
|
testing.expect_value(t, command[index], value)
|
|
}
|
|
}
|
|
|
|
Fake_Cimport_State :: struct {
|
|
calls: int,
|
|
available: bool,
|
|
infrastructure: bool,
|
|
saw_options: bool,
|
|
}
|
|
|
|
Conflict_Cimport_State :: struct {
|
|
calls: int,
|
|
}
|
|
|
|
Symbol_Conflict_Cimport_State :: struct {
|
|
calls: int,
|
|
}
|
|
|
|
fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
state := (^Fake_Cimport_State)(user_data)
|
|
state.calls += 1
|
|
state.saw_options =
|
|
len(request.include_paths) == 2 &&
|
|
request.include_paths[0] == "first/include" &&
|
|
request.include_paths[1] == "second/include" &&
|
|
len(request.defines) == 2 &&
|
|
request.defines[0] == "FIRST=1" &&
|
|
request.defines[1] == "SECOND" &&
|
|
request.target == target.DEFAULT
|
|
result := cimport.init_result(allocator)
|
|
if !state.available {
|
|
result.infrastructure = state.infrastructure
|
|
result.error_message = fmt.aprintf("fake importer unavailable", allocator=allocator)
|
|
return result
|
|
}
|
|
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
|
|
append(&result.functions, cimport.Function{
|
|
name=fmt.aprintf("fake_value", allocator=allocator),
|
|
result=cimport.Type_Id(0),
|
|
variadic=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("fake_global", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
append(&result.macros, cimport.Macro_Constant{
|
|
name=fmt.aprintf("FAKE_MAGIC", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
value={kind=.Integer, type=cimport.Type_Id(0), integer=7},
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
state := (^Conflict_Cimport_State)(user_data)
|
|
state.calls += 1
|
|
result := cimport.init_result(allocator)
|
|
kind := cimport.Type_Kind.C_Int
|
|
if strings.has_suffix(request.path, "second.h") {
|
|
kind = .C_Long
|
|
}
|
|
append(&result.types, cimport.Type{kind=kind, child=cimport.INVALID_TYPE})
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("conflict_global", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
symbol_conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
state := (^Symbol_Conflict_Cimport_State)(user_data)
|
|
state.calls += 1
|
|
result := cimport.init_result(allocator)
|
|
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
|
|
if strings.has_suffix(request.path, "variable.h") {
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("conflict_symbol", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
} else {
|
|
append(&result.functions, cimport.Function{
|
|
name=fmt.aprintf("conflict_symbol", allocator=allocator),
|
|
result=cimport.Type_Id(0),
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
}
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
main_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
result := cimport.init_result(allocator)
|
|
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("main", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
write_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result {
|
|
result := cimport.init_result(allocator)
|
|
append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE})
|
|
append(&result.variables, cimport.Variable{
|
|
name=fmt.aprintf("write", allocator=allocator),
|
|
type=cimport.Type_Id(0),
|
|
mutable=true,
|
|
reason=fmt.aprintf("", allocator=allocator),
|
|
})
|
|
result.available = true
|
|
return result
|
|
}
|
|
|
|
count_substring_occurrences :: proc(text, needle: string) -> int {
|
|
if len(needle) == 0 {
|
|
return 0
|
|
}
|
|
count := 0
|
|
for index := 0; index + len(needle) <= len(text); index += 1 {
|
|
if text[index:index + len(needle)] == needle {
|
|
count += 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
find_substring_offset :: proc(text, needle: string) -> int {
|
|
if len(needle) == 0 {
|
|
return 0
|
|
}
|
|
for index := 0; index + len(needle) <= len(text); index += 1 {
|
|
if text[index:index + len(needle)] == needle {
|
|
return index
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
find_cimport_variable :: proc(result: ^cimport.Result, name: string) -> (^cimport.Variable, bool) {
|
|
for &variable in result.variables {
|
|
if variable.name == name {
|
|
return &variable, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
count_cimport_variables :: proc(result: ^cimport.Result, name: string) -> int {
|
|
count := 0
|
|
for variable in result.variables {
|
|
if variable.name == name {
|
|
count += 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
find_cimport_macro :: proc(result: ^cimport.Result, name: string) -> (^cimport.Macro_Constant, bool) {
|
|
for ¯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)
|
|
maximum_signed_i64_literal_parses_exactly :: proc(t: ^testing.T) {
|
|
source_file := source.Source{path="test.bro", text="value :: 9223372036854775807\nmain :: func() void {}\n"}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, module.exprs[module.globals[0].expr].integer, u64(9223372036854775807))
|
|
}
|
|
|
|
@(test)
|
|
negative_constants_fold_and_accept_signed_i64_minimum :: proc(t: ^testing.T) {
|
|
text := `minimum :: -9223372036854775808
|
|
grouped i64 :: -(9223372036854775808)
|
|
folded :: -(1 + 2)
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, types.equal(hir_module.globals[0].type, types.I64))
|
|
testing.expect(t, types.equal(hir_module.globals[1].type, types.I64))
|
|
testing.expect(t, types.equal(hir_module.globals[2].type, types.I8))
|
|
testing.expect_value(t, hir_module.globals[0].static_value, i64(-9223372036854775807-1))
|
|
testing.expect_value(t, hir_module.globals[1].static_value, i64(-9223372036854775807-1))
|
|
testing.expect_value(t, hir_module.globals[2].static_value, i64(-3))
|
|
}
|
|
|
|
@(test)
|
|
constant_division_by_zero_has_a_precise_diagnostic :: proc(t: ^testing.T) {
|
|
text := `value :: 5 / 0
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_division_by_zero := false
|
|
found_overflow := false
|
|
for diagnostic in diagnostics.items {
|
|
found_division_by_zero = found_division_by_zero ||
|
|
strings.contains(diagnostic.message, "division by zero in constant expression")
|
|
found_overflow = found_overflow ||
|
|
strings.contains(diagnostic.message, "integer constant expression exceeds signed i64 range")
|
|
}
|
|
testing.expect(t, found_division_by_zero)
|
|
testing.expect(t, !found_overflow)
|
|
}
|
|
|
|
@(test)
|
|
out_of_range_negative_constants_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := `positive :: 9223372036854775808
|
|
below_minimum :: -9223372036854775809
|
|
double_minimum :: --9223372036854775808
|
|
maximum_u64 :: 18446744073709551615
|
|
beyond_u64 :: 18446744073709551616
|
|
main :: func() void {}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_signed_range := 0
|
|
found_u64_range := false
|
|
for diagnostic in diagnostics.items {
|
|
found_signed_range += 1 if strings.contains(diagnostic.message, "exceeds signed i64 range") else 0
|
|
found_u64_range = found_u64_range || strings.contains(diagnostic.message, "magnitude does not fit in u64")
|
|
}
|
|
testing.expect_value(t, found_signed_range, 4)
|
|
testing.expect(t, found_u64_range)
|
|
}
|
|
|
|
@(test)
|
|
runtime_negation_preserves_operand_type_before_result_widening :: proc(t: ^testing.T) {
|
|
text := `negate_i8 :: func(value i8) i8 {
|
|
return -value
|
|
}
|
|
widen_after_negate :: func(value i8) i16 {
|
|
return -value
|
|
}
|
|
main :: func() void {
|
|
_ = negate_i8(1)
|
|
_ = widen_after_negate(1)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i8"))
|
|
found_widen := false
|
|
for function in hir_module.functions {
|
|
if symbol.resolve(&symbols, function.name) != "widen_after_negate" {
|
|
continue
|
|
}
|
|
statement := hir_module.statements[function.body[0]]
|
|
widen := hir_module.exprs[statement.expr]
|
|
negate := hir_module.exprs[widen.left]
|
|
testing.expect_value(t, widen.kind, hir.Expr_Kind.Widen)
|
|
testing.expect(t, types.equal(widen.type, types.I16))
|
|
testing.expect_value(t, negate.kind, hir.Expr_Kind.Negate)
|
|
testing.expect(t, types.equal(negate.type, types.I8))
|
|
found_widen = true
|
|
}
|
|
testing.expect(t, found_widen)
|
|
}
|
|
|
|
@(test)
|
|
malformed_hir_references_lower_to_valid_trapped_llvm :: proc(t: ^testing.T) {
|
|
hir_module := hir.init_module()
|
|
defer hir.destroy_module(&hir_module)
|
|
append(&hir_module.exprs, hir.Expr{
|
|
kind=.Local,
|
|
type=types.I8,
|
|
target=hir.local_ref(hir.INVALID_LOCAL),
|
|
left=hir.INVALID_EXPR,
|
|
right=hir.INVALID_EXPR,
|
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
|
})
|
|
append(&hir_module.statements, hir.Stmt{
|
|
kind=.Expression,
|
|
expr=hir.Expr_Id(0),
|
|
local=hir.INVALID_LOCAL,
|
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
|
})
|
|
body := make([]hir.Stmt_Id, 1)
|
|
body[0] = hir.Stmt_Id(0)
|
|
append(&hir_module.functions, hir.Function{
|
|
name=symbol.INVALID,
|
|
link_name=strings.clone("main"),
|
|
calling_convention=.C,
|
|
implementation=.Definition,
|
|
linkage=.External,
|
|
is_main=true,
|
|
result=types.VOID,
|
|
body=body,
|
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
|
})
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
source_file := source.Source{path="test.bro", text=""}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(text)
|
|
|
|
testing.expect(t, strings.contains(text, "call void @bro.trap"))
|
|
testing.expect(t, !strings.contains(text, "%v-1"))
|
|
}
|
|
|
|
@(test)
|
|
malformed_ir_emits_traps_and_typed_sentinels :: proc(t: ^testing.T) {
|
|
module := ir.init_module()
|
|
defer ir.destroy_module(&module)
|
|
instructions := make([]ir.Instruction, 4)
|
|
instructions[0] = ir.Instruction{op=.Store, type=types.I8, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
|
|
instructions[1] = ir.Instruction{op=.Add_Checked, type=types.I32, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
|
|
instructions[2] = ir.Instruction{op=.Neg_Checked, type=types.I16, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
|
|
instructions[3] = ir.Instruction{op=.Return, type=types.I32, a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC}
|
|
append(&module.functions, ir.Function{
|
|
link_name=strings.clone("main"),
|
|
calling_convention=.C,
|
|
implementation=.Definition,
|
|
linkage=.External,
|
|
is_main=true,
|
|
result=types.I32,
|
|
instructions=instructions,
|
|
})
|
|
source_file := source.Source{path="test.bro", text=""}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
text := llvm.emit(&module, &diagnostics, &symbols)
|
|
defer delete(text)
|
|
|
|
testing.expect(t, strings.contains(text, "call void @bro.trap"))
|
|
testing.expect(t, strings.contains(text, "%v0 = add i8 0, -86"))
|
|
testing.expect(t, strings.contains(text, "%v1 = add i32 0, -1431655766"))
|
|
testing.expect(t, strings.contains(text, "%v2 = add i16 0, -21846"))
|
|
testing.expect(t, strings.contains(text, "@bro.trap(ptr %message, i64 %length) noreturn"))
|
|
testing.expect(t, !strings.contains(text, "%v-1"))
|
|
llvm_path := "/tmp/brolang-test-malformed-recovery.ll"
|
|
output := "/tmp/brolang-test-malformed-recovery"
|
|
defer _ = os.remove(llvm_path)
|
|
defer _ = os.remove(output)
|
|
testing.expect(t, os.write_entire_file(llvm_path, transmute([]byte)text))
|
|
testing.expect(t, backend.compile(llvm_path, output))
|
|
}
|
|
|
|
@(test)
|
|
hundred_thousand_term_runtime_addition_uses_iterative_pipeline :: proc(t: ^testing.T) {
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
strings.write_string(&builder, "sum :: func(value i32) i32 { return value")
|
|
for _ in 0..<100_000 {
|
|
strings.write_string(&builder, " + 1")
|
|
}
|
|
strings.write_string(&builder, " }\nmain :: func() void { _ = sum(0) }\n")
|
|
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
|
|
instruction_count := 0
|
|
for function in ir_module.functions {
|
|
instruction_count += len(function.instructions)
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, instruction_count > 100_000)
|
|
}
|
|
|
|
@(test)
|
|
deep_global_cycle_detection_uses_iterative_dfs :: proc(t: ^testing.T) {
|
|
count := 50_000
|
|
ast_module := ast.init_module()
|
|
defer ast.destroy_module(&ast_module)
|
|
source_file := source.Source{path="test.bro", text=""}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
name := symbol.intern(&symbols, "value")
|
|
state := checker.Checker{
|
|
ast_module=&ast_module,
|
|
diagnostics=&diagnostics,
|
|
symbols=&symbols,
|
|
module=hir.init_module(),
|
|
allocator=context.allocator,
|
|
}
|
|
defer hir.destroy_module(&state.module)
|
|
defer delete(state.cycle_stack)
|
|
|
|
for id in 0..<count {
|
|
append(&ast_module.globals, ast.Global{name=name})
|
|
dependencies: [dynamic]hir.Global_Id
|
|
dependencies.allocator = context.allocator
|
|
append(&dependencies, hir.global_id((id+1)%count))
|
|
append(&state.module.globals, hir.Global{name=name, dependencies=dependencies})
|
|
}
|
|
states := make([]u8, count)
|
|
defer delete(states)
|
|
checker.detect_global_cycles_visit(&state, hir.Global_Id(0), states)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 1)
|
|
for global in state.module.globals {
|
|
testing.expect(t, global.problematic)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
mutable_local_reassignment_uses_runtime_storage :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-mutable"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/mutable_local", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 3)
|
|
}
|
|
|
|
@(test)
|
|
implicit_narrowing_produces_trap_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-narrowing"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/narrowing_error", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
valid_runtime_global_initializes_before_main :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-runtime-global"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/runtime_global", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
unused_global_cycle_is_deferred :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-cycle-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/cycle_unused", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
used_global_cycle_traps :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-cycle-used"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/cycle_used", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
malformed_typed_values_still_produce_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-malformed-typed"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/malformed_typed_recovery", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
unused_invalid_function_body_is_not_checked :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-invalid-unused-function"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/invalid_unused_function", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
used_invalid_function_is_diagnosed_and_traps :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-invalid-used-function"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/invalid_used_function", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
function_mediated_problematic_global_is_deferred :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-function-global-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/function_global_unused", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
function_mediated_problematic_global_traps_when_used :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-function-global-used"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/function_global_used", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
package_files_merge_and_qualified_imports_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-basic"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/basic/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 3)
|
|
}
|
|
|
|
@(test)
|
|
unused_import_is_diagnosed_but_remains_executable :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/unused/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
unused_missing_package_does_not_trap :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-missing-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/missing_unused/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
referenced_missing_package_traps_at_reference :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-missing-used"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/missing_used/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state, stdout, stderr, _ := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}},
|
|
context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
testing.expect(t, !state.success)
|
|
testing.expect(t, strings.contains(string(stderr), "/missing_used/app/main.bro:4:6:"))
|
|
}
|
|
|
|
@(test)
|
|
imports_are_file_local :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-file-local"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/file_local/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
package_import_cycles_are_valid :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-cycle"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/cycle/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 4)
|
|
}
|
|
|
|
@(test)
|
|
imported_main_is_an_ordinary_callable_function :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-imported-main"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/imported_main/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 7)
|
|
}
|
|
|
|
@(test)
|
|
same_named_c_functions_in_packages_do_not_collide :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-c-symbols"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/c_symbols/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
invalid_root_package_preserves_existing_output :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-preserved-output"
|
|
defer _ = os.remove(output)
|
|
previous := "previous artifact"
|
|
testing.expect(t, os.write_entire_file(output, transmute([]byte)previous))
|
|
testing.expect_value(t, compiler_core.compile_package("examples/programs/prototype/main.bro", output), 2)
|
|
data, ok := os.read_entire_file(output)
|
|
defer delete(data)
|
|
testing.expect(t, ok)
|
|
testing.expect_value(t, string(data), previous)
|
|
}
|
|
|
|
@(test)
|
|
empty_root_package_is_an_infrastructure_error :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-empty"
|
|
defer _ = os.remove(output)
|
|
testing.expect_value(t, compiler_core.compile_package("examples/packages/empty", output), 2)
|
|
testing.expect(t, !os.exists(output))
|
|
}
|
|
|
|
@(test)
|
|
same_package_can_be_imported_under_distinct_aliases :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-aliases"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/aliases/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 4)
|
|
}
|
|
|
|
@(test)
|
|
duplicate_same_file_import_keeps_first_binding :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-duplicate"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/duplicate/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 2)
|
|
}
|
|
|
|
@(test)
|
|
identical_imports_in_different_files_are_independent :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-repeated"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/repeated/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 4)
|
|
}
|
|
|
|
@(test)
|
|
imports_do_not_reexport_members :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-non-transitive"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/non_transitive/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
self_import_via_dot_is_valid :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-self"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/self_import/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 5)
|
|
}
|
|
|
|
@(test)
|
|
unused_absolute_import_is_diagnosed_without_trapping :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-absolute"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/absolute/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
unreferenced_problematic_imported_global_is_deferred :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-problematic-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/problematic_unused/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
valid_eager_global_in_unused_package_still_initializes :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-eager-unused"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/eager_unused/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
cross_package_global_initialization_cycle_traps_when_used :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-global-cycle"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/global_cycle/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
cross_package_generic_specializes_from_folded_argument :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-generic"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/generic/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 128)
|
|
}
|
|
|
|
@(test)
|
|
lazy_function_body_marks_import_as_used_without_resolving_it :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-lazy-import"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/lazy_import/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
qualified_global_inference_ignores_same_named_local :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-qualified-shadow"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/qualified_shadow/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 300)
|
|
}
|
|
|
|
@(test)
|
|
cross_package_recursive_specialization_reaches_fixed_point :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-recursive"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/recursive/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
testing.expect(t, os.exists(output))
|
|
}
|
|
|
|
@(test)
|
|
imported_main_does_not_satisfy_root_main_requirement :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-missing-root-main"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/missing_root_main/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect(t, !state.success)
|
|
}
|
|
|
|
@(test)
|
|
package_llvm_is_deterministic_and_symbols_include_package_ids :: proc(t: ^testing.T) {
|
|
sources := source.init_store()
|
|
defer source.destroy_store(&sources)
|
|
diagnostics := source.init_store_diagnostics(&sources)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
ast_module, loaded := loader.load("examples/packages/c_symbols/app", &sources, &diagnostics, &symbols)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
second_llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(second_llvm_text)
|
|
|
|
testing.expect(t, loaded)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect_value(t, llvm_text, second_llvm_text)
|
|
testing.expect(t, strings.contains(llvm_text, "define signext i8 @bro_c__p1__same"))
|
|
testing.expect(t, strings.contains(llvm_text, "define signext i8 @bro_c__p2__same"))
|
|
testing.expect(t, strings.contains(llvm_text, "define i32 @main()"))
|
|
}
|
|
|
|
@(test)
|
|
invalid_default_alias_requires_an_explicit_alias :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-default-alias-invalid"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/default_alias_invalid/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
explicit_alias_allows_invalid_directory_basename :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-explicit-alias"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/explicit_alias/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 3)
|
|
}
|
|
|
|
@(test)
|
|
import_alias_conflict_is_diagnosed_without_trapping :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-alias-conflict"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/alias_conflict/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 7)
|
|
}
|
|
|
|
@(test)
|
|
empty_imported_package_is_a_source_diagnostic :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-empty-import"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/empty_import/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
non_directory_import_is_a_source_diagnostic :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-file-import"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/file_import/app", output)
|
|
testing.expect_value(t, status, 1)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
import_is_available_before_its_textual_declaration :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-import-order"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/import_order/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 6)
|
|
}
|
|
|
|
@(test)
|
|
deeply_nested_child_packages_load_recursively :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-package-deep"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/packages/deep/app", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 9)
|
|
}
|
|
|
|
@(test)
|
|
function_returning_only_in_if_branch_is_diagnosed :: proc(t: ^testing.T) {
|
|
text := `classify :: func(n i32) i32 {
|
|
if n > 0 {
|
|
return 1
|
|
}
|
|
}
|
|
main :: func() void {
|
|
_ = classify(5)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "does not return a value")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
function_returning_in_both_if_arms_is_accepted :: proc(t: ^testing.T) {
|
|
text := `classify :: func(n i32) i32 {
|
|
if n > 0 {
|
|
return 1
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
main :: func() void {
|
|
_ = classify(5)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "does not return a value")
|
|
}
|
|
testing.expect(t, !found)
|
|
}
|
|
|
|
@(test)
|
|
function_returning_after_if_is_accepted :: proc(t: ^testing.T) {
|
|
text := `classify :: func(n i32) i32 {
|
|
if n > 0 {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
main :: func() void {
|
|
_ = classify(5)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "does not return a value")
|
|
}
|
|
testing.expect(t, !found)
|
|
}
|
|
|
|
@(test)
|
|
conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-conditional-unwrap"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/conditional_unwrap", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
// Single unwrap, guarded two/three-value unwraps, optional pointers, false
|
|
// guards, and failed short-circuit chains preserve the expected total.
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
conditional_unwrap_parser_captures_guard_and_parenthesized_chain :: proc(t: ^testing.T) {
|
|
text := `main :: func() void {
|
|
first ?i32 = 1
|
|
second ?i32 = 2
|
|
if (first and second) |a, b : a == 1 and b == 2| {
|
|
_ = a
|
|
_ = b
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
statement := module.statements[module.functions[0].body[2]]
|
|
testing.expect_value(t, statement.kind, ast.Stmt_Kind.If)
|
|
testing.expect_value(t, len(statement.captures), 2)
|
|
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.And)
|
|
testing.expect(t, module.exprs[statement.expr].parenthesized)
|
|
testing.expect(t, statement.guard != ast.INVALID_EXPR)
|
|
testing.expect_value(t, module.exprs[statement.guard].kind, ast.Expr_Kind.And)
|
|
}
|
|
|
|
@(test)
|
|
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)
|
|
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, "unresolved global 'earlier'")
|
|
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'")
|
|
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'value'")
|
|
capture_scope += 1 if strings.contains(diagnostic.message, "unresolved global 'value'") else 0
|
|
}
|
|
testing.expect_value(t, count_mismatches, 2)
|
|
testing.expect(t, duplicate)
|
|
testing.expect(t, non_optional)
|
|
testing.expect(t, guard)
|
|
testing.expect(t, outer_operand_scope)
|
|
testing.expect(t, immutable)
|
|
testing.expect(t, redeclaration)
|
|
testing.expect_value(t, capture_scope, 2)
|
|
}
|
|
|
|
@(test)
|
|
if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) {
|
|
// The binding `v` is usable in the then-block but not in the else-block.
|
|
text := `main :: func() i32 {
|
|
a ?i32 = 1
|
|
if a |v| {
|
|
return v
|
|
} else {
|
|
return v
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "unresolved global 'v'")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
if_unwrap_binding_is_immutable :: proc(t: ^testing.T) {
|
|
text := `main :: func() i32 {
|
|
a ?i32 = 1
|
|
if a |v| {
|
|
v = 2
|
|
return v
|
|
}
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "cannot assign immutable local 'v'")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
while_loops_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-while-loop"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/while_loop", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
while_loop_diagnostics_cover_condition_update_and_scope :: proc(t: ^testing.T) {
|
|
text := `bad_condition :: func() void {
|
|
while 1 {}
|
|
}
|
|
bad_unresolved :: func() void {
|
|
while false : missing = 1 {}
|
|
}
|
|
bad_immutable :: func() void {
|
|
i :: 0
|
|
while false : i = i + 1 {}
|
|
}
|
|
bad_body_scope :: func() void {
|
|
running :: false
|
|
while running : i = 1 {
|
|
i u32 = 0
|
|
}
|
|
}
|
|
bad_declaration_update :: func() void {
|
|
while false : i u32 = 0 {}
|
|
}
|
|
bad_missing_update :: func() void {
|
|
while false : {}
|
|
}
|
|
main :: func() void {
|
|
bad_condition()
|
|
bad_unresolved()
|
|
bad_immutable()
|
|
bad_body_scope()
|
|
bad_declaration_update()
|
|
bad_missing_update()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
non_bool := false
|
|
unresolved_missing := false
|
|
unresolved_body_local := false
|
|
immutable := false
|
|
disallowed := false
|
|
missing := false
|
|
for diagnostic in diagnostics.items {
|
|
non_bool = non_bool || strings.contains(diagnostic.message, "'while' condition must be a bool")
|
|
unresolved_missing = unresolved_missing || strings.contains(diagnostic.message, "cannot assign unresolved local 'missing'")
|
|
unresolved_body_local = unresolved_body_local || strings.contains(diagnostic.message, "cannot assign unresolved local 'i'")
|
|
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'i'")
|
|
disallowed = disallowed || strings.contains(diagnostic.message, "while update must be an assignment, sink, or expression statement")
|
|
missing = missing || strings.contains(diagnostic.message, "expected a while update statement after ':'")
|
|
}
|
|
testing.expect(t, non_bool)
|
|
testing.expect(t, unresolved_missing)
|
|
testing.expect(t, unresolved_body_local)
|
|
testing.expect(t, immutable)
|
|
testing.expect(t, disallowed)
|
|
testing.expect(t, missing)
|
|
}
|
|
|
|
@(test)
|
|
while_true_and_potential_fallthrough_have_distinct_return_analysis :: proc(t: ^testing.T) {
|
|
text := `forever :: func() i32 {
|
|
while true {}
|
|
}
|
|
maybe :: func(run bool) i32 {
|
|
while run {
|
|
return 1
|
|
}
|
|
}
|
|
main :: func() void {
|
|
if false {
|
|
_ = forever()
|
|
}
|
|
_ = maybe(false)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
missing_return_count := 0
|
|
for diagnostic in diagnostics.items {
|
|
if strings.contains(diagnostic.message, "does not return a value") {
|
|
missing_return_count += 1
|
|
}
|
|
}
|
|
testing.expect_value(t, missing_return_count, 1)
|
|
}
|
|
|
|
@(test)
|
|
while_loop_allocas_are_emitted_in_the_entry_block :: proc(t: ^testing.T) {
|
|
text := `main :: func() i32 {
|
|
i u32 = 0
|
|
while i < 2 and true : i = i + 1 {
|
|
value u32 = i
|
|
_ = value
|
|
}
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
|
|
testing.expect(t, first_loop_label >= 0)
|
|
alloca_count := 0
|
|
for function in ir_module.functions {
|
|
if !function.is_main {
|
|
continue
|
|
}
|
|
for instruction, instruction_index in function.instructions {
|
|
if instruction.op != .Alloca {
|
|
continue
|
|
}
|
|
alloca_count += 1
|
|
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
|
|
offset := find_substring_offset(llvm_text, needle)
|
|
testing.expect(t, offset >= 0 && offset < first_loop_label)
|
|
}
|
|
}
|
|
testing.expect(t, alloca_count >= 3)
|
|
}
|
|
|
|
@(test)
|
|
for_loop_tokens_and_parser_capture_range_shape :: proc(t: ^testing.T) {
|
|
text := `main :: func() void {
|
|
for 0..4 |value| {
|
|
_ = value
|
|
}
|
|
items [1]mut i32 = [1]
|
|
for (&items) |@item, index| {
|
|
_ = item
|
|
_ = index
|
|
}
|
|
for 0..=1 |inclusive| {
|
|
_ = inclusive
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
|
|
for_count := 0
|
|
range_count := 0
|
|
inclusive_count := 0
|
|
for tok in stream.items {
|
|
#partial switch tok.kind {
|
|
case .Keyword_For: for_count += 1
|
|
case .Range: range_count += 1
|
|
case .Range_Inclusive: inclusive_count += 1
|
|
case:
|
|
}
|
|
}
|
|
testing.expect_value(t, for_count, 3)
|
|
testing.expect_value(t, range_count, 1)
|
|
testing.expect_value(t, inclusive_count, 1)
|
|
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
function := module.functions[0]
|
|
first := module.statements[function.body[0]]
|
|
second := module.statements[function.body[2]]
|
|
third := module.statements[function.body[3]]
|
|
testing.expect_value(t, first.kind, ast.Stmt_Kind.For)
|
|
testing.expect(t, !first.pointer_capture)
|
|
testing.expect_value(t, module.exprs[first.expr].kind, ast.Expr_Kind.Range)
|
|
testing.expect_value(t, module.exprs[first.expr].integer, u64(0))
|
|
testing.expect_value(t, second.kind, ast.Stmt_Kind.For)
|
|
testing.expect(t, second.pointer_capture)
|
|
testing.expect(t, symbol.is_valid(second.index_name))
|
|
testing.expect_value(t, third.kind, ast.Stmt_Kind.For)
|
|
testing.expect_value(t, module.exprs[third.expr].integer, u64(1))
|
|
}
|
|
|
|
@(test)
|
|
range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) {
|
|
text := `main :: func() void {
|
|
limit :: 3
|
|
for 0..limit + 1 |bad| {
|
|
_ = bad
|
|
}
|
|
for 0..(limit + 1) |good| {
|
|
_ = good
|
|
}
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
found := 0
|
|
for diagnostic in diagnostics.items {
|
|
found += 1 if strings.contains(diagnostic.message, "range bounds with operators must be parenthesized") else 0
|
|
}
|
|
testing.expect_value(t, found, 1)
|
|
}
|
|
|
|
@(test)
|
|
parser_diagnoses_malformed_for_captures :: proc(t: ^testing.T) {
|
|
cases := [4]struct {
|
|
text: string,
|
|
needle: string,
|
|
}{
|
|
{`main :: func() void {
|
|
for [1] item {}
|
|
}
|
|
`, "expected '|' before for-loop captures"},
|
|
{`main :: func() void {
|
|
for [1] |@| {}
|
|
}
|
|
`, "expected a for-loop item capture"},
|
|
{`main :: func() void {
|
|
for [1] |item,| {}
|
|
}
|
|
`, "expected an index capture after ','"},
|
|
{`main :: func() void {
|
|
for [1] |item {}
|
|
}
|
|
`, "expected '|' to close for-loop captures"},
|
|
}
|
|
for test_case in cases {
|
|
source_file := source.Source{path="test.bro", text=test_case.text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
symbols := symbol.init_table()
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, test_case.needle)
|
|
}
|
|
testing.expect(t, found)
|
|
|
|
ast.destroy_module(&module)
|
|
delete(stream.items)
|
|
symbol.destroy_table(&symbols)
|
|
source.destroy_diagnostics(&diagnostics)
|
|
}
|
|
}
|
|
|
|
@(test)
|
|
for_loops_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-for-loop"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/for_loop", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
range_loop_edges_compile_and_run :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-for-loop-edges"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/for_loop_edges", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 42)
|
|
}
|
|
|
|
@(test)
|
|
for_loop_diagnostics_cover_iterables_captures_and_scope :: proc(t: ^testing.T) {
|
|
text := `bad_iterable :: func() void {
|
|
for 1 |item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_array_pointer_capture :: func() void {
|
|
for [1] |@item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_range_pointer_capture :: func() void {
|
|
for 0..1 |@item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_range_index_capture :: func() void {
|
|
for 0..1 |item, index| {
|
|
_ = item
|
|
_ = index
|
|
}
|
|
}
|
|
bad_duplicate_capture :: func() void {
|
|
for [1] |item, item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_capture_redeclaration :: func() void {
|
|
for [1] |item| {
|
|
item i32 = 2
|
|
_ = item
|
|
}
|
|
}
|
|
bad_capture_assignment :: func() void {
|
|
for [1] |item| {
|
|
item = 2
|
|
}
|
|
}
|
|
bad_immutable_pointer_capture :: func() void {
|
|
items :: [1]
|
|
for (&items) |@item| {
|
|
item^ = 2
|
|
}
|
|
}
|
|
bad_scope :: func() void {
|
|
for [1] |item| {
|
|
_ = item
|
|
}
|
|
_ = item
|
|
}
|
|
bad_integer_bounds :: func() void {
|
|
start i32 = 0
|
|
end u32 = 1
|
|
for start..end |item| {
|
|
_ = item
|
|
}
|
|
}
|
|
bad_float_bounds :: func() void {
|
|
for 0.0..1.0 |item| {
|
|
_ = item
|
|
}
|
|
}
|
|
main :: func() void {
|
|
bad_iterable()
|
|
bad_array_pointer_capture()
|
|
bad_range_pointer_capture()
|
|
bad_range_index_capture()
|
|
bad_duplicate_capture()
|
|
bad_capture_redeclaration()
|
|
bad_capture_assignment()
|
|
bad_immutable_pointer_capture()
|
|
bad_scope()
|
|
bad_integer_bounds()
|
|
bad_float_bounds()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
unsupported := false
|
|
array_pointer := false
|
|
range_pointer := false
|
|
range_index := false
|
|
duplicate_capture := false
|
|
redeclaration := false
|
|
immutable := false
|
|
immutable_pointer := false
|
|
scope := false
|
|
integer_bounds := 0
|
|
for diagnostic in diagnostics.items {
|
|
unsupported = unsupported || strings.contains(diagnostic.message, "for-loop iterable must be a range")
|
|
array_pointer = array_pointer || strings.contains(diagnostic.message, "pointer capture over an array requires")
|
|
range_pointer = range_pointer || strings.contains(diagnostic.message, "range loops do not support pointer captures")
|
|
range_index = range_index || strings.contains(diagnostic.message, "range loops do not support index captures")
|
|
duplicate_capture = duplicate_capture || strings.contains(diagnostic.message, "for-loop captures must have distinct names")
|
|
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'")
|
|
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'item'")
|
|
immutable_pointer = immutable_pointer || strings.contains(diagnostic.message, "assignment target is not writable")
|
|
scope = scope || strings.contains(diagnostic.message, "unresolved global 'item'")
|
|
integer_bounds += 1 if strings.contains(diagnostic.message, "range bounds must be compatible concrete integers") else 0
|
|
}
|
|
testing.expect(t, unsupported)
|
|
testing.expect(t, array_pointer)
|
|
testing.expect(t, range_pointer)
|
|
testing.expect(t, range_index)
|
|
testing.expect(t, duplicate_capture)
|
|
testing.expect(t, redeclaration)
|
|
testing.expect(t, immutable)
|
|
testing.expect(t, immutable_pointer)
|
|
testing.expect(t, scope)
|
|
testing.expect_value(t, integer_bounds, 2)
|
|
}
|
|
|
|
@(test)
|
|
for_pointer_capture_respects_pointer_and_array_mutability :: proc(t: ^testing.T) {
|
|
text := `readonly :: func() void {
|
|
values [1]mut i32 = [1]
|
|
items @[1]mut i32 = &values
|
|
items[0] = 7
|
|
for items |@item| {
|
|
item^ = 7
|
|
}
|
|
}
|
|
writable :: func() void {
|
|
values [1]mut i32 = [1]
|
|
items @mut [1]mut i32 = &values
|
|
items[0] = 7
|
|
for items |@item| {
|
|
item^ = 7
|
|
}
|
|
}
|
|
main :: func() void {
|
|
readonly()
|
|
writable()
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
readonly_errors := 0
|
|
for diagnostic in diagnostics.items {
|
|
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
|
|
}
|
|
testing.expect_value(t, readonly_errors, 2)
|
|
}
|
|
|
|
@(test)
|
|
pointer_field_passthrough_respects_pointee_mutability :: proc(t: ^testing.T) {
|
|
text := `Point :: struct {
|
|
x i32
|
|
}
|
|
readonly :: func(point @Point) i32 {
|
|
return point.x
|
|
}
|
|
bad_write :: func(point @Point) void {
|
|
point.x = 7
|
|
}
|
|
writable :: func(point @mut Point) void {
|
|
point.x += 1
|
|
}
|
|
main :: func() i32 {
|
|
point Point = Point { x = 41 }
|
|
writable(&point)
|
|
bad_write(&point)
|
|
return readonly(&point)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
readonly_errors := 0
|
|
for diagnostic in diagnostics.items {
|
|
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
|
|
}
|
|
testing.expect_value(t, readonly_errors, 1)
|
|
}
|
|
|
|
@(test)
|
|
equal_range_returns_infer_a_usable_result_type :: proc(t: ^testing.T) {
|
|
text := `choose :: func(first bool) 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
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&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 /= 4
|
|
unsigned u32 = 24
|
|
unsigned += 6
|
|
unsigned -= 2
|
|
unsigned *= 3
|
|
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], 3)
|
|
|
|
add_count := 0
|
|
sub_count := 0
|
|
mul_count := 0
|
|
div_count := 0
|
|
for instruction in ir_module.functions[0].instructions {
|
|
#partial switch instruction.op {
|
|
case .Add_Checked: add_count += 1
|
|
case .Sub_Checked: sub_count += 1
|
|
case .Mul_Checked: mul_count += 1
|
|
case .Div_Checked: div_count += 1
|
|
case:
|
|
}
|
|
}
|
|
testing.expect_value(t, add_count, 3)
|
|
testing.expect_value(t, sub_count, 3)
|
|
testing.expect_value(t, mul_count, 3)
|
|
testing.expect_value(t, div_count, 3)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_rejects_narrowing_and_mixed_numeric_families :: proc(t: ^testing.T) {
|
|
text := `main :: func() void {
|
|
narrow i8 = 1
|
|
wide i32 = 2
|
|
narrow += wide
|
|
signed i32 = 3
|
|
unsigned u32 = 4
|
|
signed += unsigned
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_narrowing := false
|
|
found_mixed_family := false
|
|
for diagnostic in diagnostics.items {
|
|
found_narrowing = found_narrowing ||
|
|
strings.contains(diagnostic.message, "cannot implicitly convert i32 to i8")
|
|
found_mixed_family = found_mixed_family ||
|
|
strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
|
|
}
|
|
testing.expect(t, found_narrowing)
|
|
testing.expect(t, found_mixed_family)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_compiles_and_runs :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-compound"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/compound_assignment", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 23)
|
|
}
|
|
|
|
@(test)
|
|
binary_arithmetic_rejects_non_numeric_operands :: proc(t: ^testing.T) {
|
|
text := `main :: func() i32 {
|
|
a i32 = 1
|
|
b u32 = 2
|
|
_ = a / b
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
compound_assignment_requires_writable_target :: proc(t: ^testing.T) {
|
|
text := `main :: func() i32 {
|
|
x :: 5
|
|
x += 1
|
|
return x
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found := false
|
|
for diagnostic in diagnostics.items {
|
|
found = found || strings.contains(diagnostic.message, "assignment target is not writable")
|
|
}
|
|
testing.expect(t, found)
|
|
}
|
|
|
|
@(test)
|
|
checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) {
|
|
text := `main :: func() i32 {
|
|
a i32 = 10
|
|
b i32 = 3
|
|
c i32 = a - b
|
|
return c / b
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i32"))
|
|
testing.expect(t, strings.contains(llvm_text, "sdiv i32"))
|
|
testing.expect(t, strings.contains(llvm_text, "divzero_trap"))
|
|
testing.expect(t, strings.contains(llvm_text, "divovf_trap"))
|
|
}
|
|
|
|
@(test)
|
|
distinct_types_preserve_nominal_identity_and_backing_representation :: proc(t: ^testing.T) {
|
|
text := `Point :: struct {
|
|
x i32
|
|
y i32
|
|
}
|
|
UserID :: distinct u32
|
|
OtherID :: distinct u32
|
|
PointID :: distinct Point
|
|
Bytes :: distinct [2]u8
|
|
WrappedID :: distinct UserID
|
|
static_id UserID :: UserID(42)
|
|
take :: func(value UserID) UserID {
|
|
return value
|
|
}
|
|
main :: func() i32 {
|
|
id UserID :: UserID(7)
|
|
copy UserID = take(id)
|
|
maybe ?UserID = copy
|
|
pointer @UserID = ©
|
|
point PointID :: PointID(Point { x = 1, y = 2 })
|
|
bytes Bytes :: Bytes([3, 4])
|
|
wrapped WrappedID :: WrappedID(id)
|
|
_ = maybe
|
|
_ = pointer
|
|
_ = point
|
|
_ = bytes
|
|
_ = wrapped
|
|
return 0
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
user_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "UserID")))
|
|
other_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "OtherID")))
|
|
point_id := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "PointID")))
|
|
point := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Point")))
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, user_id != other_id)
|
|
testing.expect(t, user_id != types.U32)
|
|
testing.expect(t, types.is_distinct(user_id, &ast_module.type_store))
|
|
testing.expect_value(t, types.runtime_representation(user_id, &ast_module.type_store), types.U32)
|
|
testing.expect_value(t, types.runtime_representation(point_id, &ast_module.type_store), point)
|
|
testing.expect_value(t, types.size(user_id, &ast_module.type_store), types.size(types.U32, &ast_module.type_store))
|
|
testing.expect(t, hir_module.globals[0].is_static)
|
|
testing.expect_value(t, hir_module.globals[0].static_value, i64(42))
|
|
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i32 42"))
|
|
testing.expect(t, strings.contains(llvm_text, "select i1 true, i32"))
|
|
|
|
retype_count := 0
|
|
for function in ir_module.functions {
|
|
for instruction in function.instructions {
|
|
retype_count += 1 if instruction.op == .Retype else 0
|
|
}
|
|
}
|
|
testing.expect_value(t, retype_count, 4)
|
|
}
|
|
|
|
@(test)
|
|
distinct_types_reject_implicit_conversions_operators_and_invalid_backings :: proc(t: ^testing.T) {
|
|
text := `Opaque :: c_struct
|
|
UserID :: distinct u32
|
|
OtherID :: distinct u32
|
|
BadInt :: distinct int
|
|
BadVoid :: distinct void
|
|
BadFunction :: distinct c_func() void
|
|
BadOpaque :: distinct Opaque
|
|
foreign :: c_func(value UserID) void
|
|
foreign_pointer :: c_func(value @UserID) void
|
|
main :: func() void {
|
|
raw u32 = 1
|
|
id UserID = raw
|
|
backing u32 = UserID(2)
|
|
other OtherID = UserID(3)
|
|
narrow u8 = 4
|
|
_ = UserID(narrow)
|
|
_ = UserID()
|
|
_ = UserID(1, 2)
|
|
left UserID :: UserID(5)
|
|
right UserID :: UserID(6)
|
|
_ = left + right
|
|
_ = left == right
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
invalid_backing_count := 0
|
|
implicit_conversion_count := 0
|
|
found_exact := false
|
|
found_arity := false
|
|
found_arithmetic := false
|
|
found_comparison := false
|
|
foreign_signature_count := 0
|
|
for diagnostic in diagnostics.items {
|
|
invalid_backing_count += 1 if strings.contains(diagnostic.message, "requires a concrete runtime backing type") else 0
|
|
implicit_conversion_count += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0
|
|
found_exact = found_exact || strings.contains(diagnostic.message, "requires an exact u32 value, got u8")
|
|
found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 argument")
|
|
found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
|
|
found_comparison = found_comparison || strings.contains(diagnostic.message, "comparison requires compatible numeric operands")
|
|
foreign_signature_count += 1 if strings.contains(diagnostic.message, "requires concrete parameter types") else 0
|
|
}
|
|
testing.expect_value(t, invalid_backing_count, 4)
|
|
testing.expect(t, implicit_conversion_count >= 3)
|
|
testing.expect(t, found_exact)
|
|
testing.expect(t, found_arity)
|
|
testing.expect(t, found_arithmetic)
|
|
testing.expect(t, found_comparison)
|
|
testing.expect_value(t, foreign_signature_count, 2)
|
|
}
|
|
|
|
@(test)
|
|
distinct_type_construction_defers_to_callable_names :: proc(t: ^testing.T) {
|
|
text := `Value :: distinct u32
|
|
Value :: func(value i32) i32 {
|
|
return value
|
|
}
|
|
main :: func() i32 {
|
|
return Value(42)
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_call := false
|
|
found_retype := false
|
|
for expr in hir_module.exprs {
|
|
found_call = found_call || expr.kind == .Call
|
|
found_retype = found_retype || expr.kind == .Retype
|
|
}
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, found_call)
|
|
testing.expect(t, !found_retype)
|
|
}
|
|
|
|
@(test)
|
|
distinct_types_compile_and_run_across_packages :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-distinct-types"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/distinct_types", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(test)
|
|
native_enums_preserve_identity_members_and_integer_representation :: proc(t: ^testing.T) {
|
|
text := `Animal :: enum {
|
|
dog
|
|
cat
|
|
bird
|
|
}
|
|
Nat :: enum(u16) {
|
|
one = 1
|
|
two
|
|
five = 5
|
|
}
|
|
global Animal :: Animal.dog
|
|
take :: func(value Animal) Animal {
|
|
return value
|
|
}
|
|
identity :: c_func(value Nat) Nat {
|
|
return value
|
|
}
|
|
variadic :: c_func(marker c_int, ...) c_int
|
|
main :: func() i32 {
|
|
value Animal = .cat
|
|
values [2]Animal :: [.dog, Animal.bird]
|
|
number Nat = identity(.two)
|
|
_ = variadic(0, number)
|
|
if take(value) == Animal.cat and values[0] != values[1] and number == Nat.two {
|
|
return 0
|
|
}
|
|
return 1
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
ir_module := lower.lower(&hir_module)
|
|
defer ir.destroy_module(&ir_module)
|
|
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
|
defer delete(llvm_text)
|
|
|
|
animal := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Animal")))
|
|
nat := types.find_named(&ast_module.type_store, 0, u32(symbol.intern(&symbols, "Nat")))
|
|
animal_node, animal_ok := types.node(&ast_module.type_store, animal)
|
|
nat_node, nat_ok := types.node(&ast_module.type_store, nat)
|
|
animal_members := types.enum_members_for(&ast_module.type_store, animal)
|
|
nat_members := types.enum_members_for(&ast_module.type_store, nat)
|
|
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, animal_ok && nat_ok)
|
|
testing.expect(t, types.is_enum(animal, &ast_module.type_store))
|
|
testing.expect_value(t, animal_node.child, types.U8)
|
|
testing.expect(t, !animal_node.explicit_backing)
|
|
testing.expect_value(t, nat_node.child, types.U16)
|
|
testing.expect(t, nat_node.explicit_backing)
|
|
testing.expect_value(t, len(animal_members), 3)
|
|
testing.expect_value(t, animal_members[0].value, i128(0))
|
|
testing.expect_value(t, animal_members[2].value, i128(2))
|
|
testing.expect_value(t, nat_members[0].value, i128(1))
|
|
testing.expect_value(t, nat_members[1].value, i128(2))
|
|
testing.expect_value(t, nat_members[2].value, i128(5))
|
|
testing.expect_value(t, types.runtime_representation(animal, &ast_module.type_store), types.U8)
|
|
testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(1))
|
|
testing.expect(t, hir_module.globals[0].is_static)
|
|
testing.expect_value(t, hir_module.globals[0].static_value, i64(0))
|
|
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i8 0"))
|
|
found_promotion := false
|
|
for function in ir_module.functions {
|
|
for instruction in function.instructions {
|
|
found_promotion = found_promotion || instruction.op == .C_Vararg_Promote
|
|
}
|
|
}
|
|
testing.expect(t, found_promotion)
|
|
}
|
|
|
|
@(test)
|
|
unbacked_enum_selects_the_smallest_fitting_unsigned_backing :: proc(t: ^testing.T) {
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
strings.write_string(&builder, "Large :: enum {\n")
|
|
for index in 0..<257 {
|
|
fmt.sbprintf(&builder, "value_%d\n", index)
|
|
}
|
|
strings.write_string(&builder, "}\nmain :: func() void {}\n")
|
|
source_file := source.Source{path="test.bro", text=strings.to_string(builder)}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&module)
|
|
|
|
large := types.find_named(&module.type_store, 0, u32(symbol.intern(&symbols, "Large")))
|
|
item, ok := types.node(&module.type_store, large)
|
|
testing.expect_value(t, len(diagnostics.items), 0)
|
|
testing.expect(t, ok)
|
|
testing.expect_value(t, item.child, types.U16)
|
|
testing.expect_value(t, len(types.enum_members_for(&module.type_store, large)), 257)
|
|
}
|
|
|
|
@(test)
|
|
native_enum_invalid_declarations_and_operations_are_diagnosed :: proc(t: ^testing.T) {
|
|
text := `Empty :: enum {}
|
|
Dense :: enum {
|
|
zero = 0
|
|
one
|
|
}
|
|
BadBacking :: enum(f32) {
|
|
value
|
|
}
|
|
Duplicate :: enum(u8) {
|
|
value
|
|
value
|
|
}
|
|
Jumbled :: enum(i8) {
|
|
second = 2
|
|
first = 1
|
|
}
|
|
Overflow :: enum(u8) {
|
|
value = 256
|
|
}
|
|
Other :: enum {
|
|
value
|
|
}
|
|
foreign :: c_func(value Dense) void
|
|
allowed :: c_func(value Overflow) Overflow
|
|
main :: func() void {
|
|
dense Dense = Other.value
|
|
_ = Dense.zero + Dense.one
|
|
_ = Dense.zero < Dense.one
|
|
_ = Dense.missing
|
|
_ = .zero
|
|
_ = dense
|
|
}
|
|
`
|
|
source_file := source.Source{path="test.bro", text=text}
|
|
diagnostics := source.init_diagnostics(&source_file)
|
|
defer source.destroy_diagnostics(&diagnostics)
|
|
symbols := symbol.init_table()
|
|
defer symbol.destroy_table(&symbols)
|
|
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
|
defer delete(stream.items)
|
|
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
|
defer ast.destroy_module(&ast_module)
|
|
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
|
defer hir.destroy_module(&hir_module)
|
|
|
|
found_empty := false
|
|
found_unbacked_value := false
|
|
found_backing := false
|
|
found_duplicate := false
|
|
found_order := false
|
|
found_overflow := false
|
|
found_foreign := false
|
|
found_conversion := false
|
|
found_arithmetic := false
|
|
found_comparison := false
|
|
found_member := false
|
|
found_context := false
|
|
for diagnostic in diagnostics.items {
|
|
found_empty = found_empty || strings.contains(diagnostic.message, "require at least one member")
|
|
found_unbacked_value = found_unbacked_value || strings.contains(diagnostic.message, "explicit enum values require a backing type")
|
|
found_backing = found_backing || strings.contains(diagnostic.message, "requires a concrete integer backing type")
|
|
found_duplicate = found_duplicate || strings.contains(diagnostic.message, "duplicate enum member")
|
|
found_order = found_order || strings.contains(diagnostic.message, "strictly increasing")
|
|
found_overflow = found_overflow || strings.contains(diagnostic.message, "does not fit in u8")
|
|
found_foreign = found_foreign || strings.contains(diagnostic.message, "requires concrete parameter types")
|
|
found_conversion = found_conversion || strings.contains(diagnostic.message, "cannot implicitly convert")
|
|
found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands")
|
|
found_comparison = found_comparison || strings.contains(diagnostic.message, "enum values only support")
|
|
found_member = found_member || strings.contains(diagnostic.message, "unknown enum member")
|
|
found_context = found_context || strings.contains(diagnostic.message, "requires an enum context")
|
|
}
|
|
testing.expect(t, found_empty)
|
|
testing.expect(t, found_unbacked_value)
|
|
testing.expect(t, found_backing)
|
|
testing.expect(t, found_duplicate)
|
|
testing.expect(t, found_order)
|
|
testing.expect(t, found_overflow)
|
|
testing.expect(t, found_foreign)
|
|
testing.expect(t, found_conversion)
|
|
testing.expect(t, found_arithmetic)
|
|
testing.expect(t, found_comparison)
|
|
testing.expect(t, found_member)
|
|
testing.expect(t, found_context)
|
|
}
|
|
|
|
@(test)
|
|
native_enums_compile_and_run_across_packages :: proc(t: ^testing.T) {
|
|
output := "/tmp/brolang-test-enums"
|
|
defer _ = os.remove(output)
|
|
status := compiler_core.compile_package("examples/programs/enums", output)
|
|
testing.expect_value(t, status, 0)
|
|
state := run_executable(output)
|
|
testing.expect_value(t, state.exit_code, 0)
|
|
}
|
|
|
|
@(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)
|
|
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)})
|
|
|
|
// 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})
|
|
|
|
// typedef aliases: a scalar and a callback function 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)})
|
|
|
|
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)})
|
|
|
|
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"))
|
|
// 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, "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)
|
|
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(v 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)
|
|
_ = 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(v 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(v 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(v u16) void {}
|
|
take_f32 :: func(v 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)
|
|
}
|