initial draft
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
package main
|
||||
|
||||
import compiler_core "./compiler"
|
||||
import "./compiler/ast"
|
||||
import "./compiler/backend"
|
||||
import "./compiler/checker"
|
||||
import "./compiler/hir"
|
||||
import "./compiler/ir"
|
||||
import "./compiler/lexer"
|
||||
import "./compiler/llvm"
|
||||
import "./compiler/lower"
|
||||
import "./compiler/parser"
|
||||
import "./compiler/source"
|
||||
import "./compiler/token"
|
||||
import "./compiler/types"
|
||||
import "core:os"
|
||||
import "core:os/os2"
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
|
||||
@(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)
|
||||
stream := lexer.lex(&source_file, &diagnostics)
|
||||
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)
|
||||
stream := lexer.lex(&source_file, &diagnostics)
|
||||
defer delete(stream.items)
|
||||
module := parser.parse(&stream, &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)
|
||||
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)
|
||||
stream := lexer.lex(&source_file, &diagnostics)
|
||||
defer delete(stream.items)
|
||||
ast_module := parser.parse(&stream, &diagnostics)
|
||||
defer ast.destroy_module(&ast_module)
|
||||
hir_module := checker.check(&ast_module, &diagnostics)
|
||||
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)
|
||||
defer delete(llvm_text)
|
||||
second_llvm_text := llvm.emit(&ir_module, &diagnostics)
|
||||
defer delete(second_llvm_text)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect_value(t, llvm_text, second_llvm_text)
|
||||
testing.expect(t, strings.contains(llvm_text, "define i8 @sum_c__i8__i8"))
|
||||
testing.expect(t, strings.contains(llvm_text, "define internal fastcc i8 @bro__sum_bro__i8__i8"))
|
||||
testing.expect(t, strings.contains(llvm_text, "@llvm.sadd.with.overflow.i8"))
|
||||
}
|
||||
|
||||
@(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)
|
||||
stream := lexer.lex(&source_file, &diagnostics)
|
||||
defer delete(stream.items)
|
||||
ast_module := parser.parse(&stream, &diagnostics)
|
||||
defer ast.destroy_module(&ast_module)
|
||||
hir_module := checker.check(&ast_module, &diagnostics)
|
||||
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)
|
||||
defer delete(llvm_text)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect(t, strings.contains(llvm_text, "@bro__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 && 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)
|
||||
stream := lexer.lex(&source_file, &diagnostics)
|
||||
defer delete(stream.items)
|
||||
ast_module := parser.parse(&stream, &diagnostics)
|
||||
defer ast.destroy_module(&ast_module)
|
||||
hir_module := checker.check(&ast_module, &diagnostics)
|
||||
defer hir.destroy_module(&hir_module)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
found := false
|
||||
for function in hir_module.functions {
|
||||
if 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)
|
||||
stream := lexer.lex(&source_file, &diagnostics)
|
||||
defer delete(stream.items)
|
||||
module := parser.parse(&stream, &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, 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)
|
||||
stream := lexer.lex(&source_file, &diagnostics)
|
||||
defer delete(stream.items)
|
||||
ast_module := parser.parse(&stream, &diagnostics)
|
||||
defer ast.destroy_module(&ast_module)
|
||||
hir_module := checker.check(&ast_module, &diagnostics)
|
||||
defer hir.destroy_module(&hir_module)
|
||||
|
||||
done_id, main_id := -1, -1
|
||||
for function, id in hir_module.functions {
|
||||
if function.name == "done" {
|
||||
done_id = id
|
||||
} else if function.name == "main" {
|
||||
main_id = id
|
||||
}
|
||||
}
|
||||
testing.expect(t, done_id >= 0)
|
||||
testing.expect(t, main_id >= 0)
|
||||
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].kind, hir.Stmt_Kind.Return)
|
||||
testing.expect_value(t, hir_module.statements[hir_module.functions[done_id].body[0]].expr, -1)
|
||||
main := hir_module.functions[main_id]
|
||||
testing.expect_value(t, hir_module.statements[main.body[0]].kind, hir.Stmt_Kind.Expression)
|
||||
testing.expect_value(t, hir_module.statements[main.body[1]].kind, hir.Stmt_Kind.Sink)
|
||||
testing.expect_value(t, hir_module.statements[main.body[2]].kind, hir.Stmt_Kind.Trap)
|
||||
}
|
||||
|
||||
@(test)
|
||||
recursive_specialization_reaches_a_fixed_point :: proc(t: ^testing.T) {
|
||||
text := `a :: func(value int) i32 {
|
||||
return b(value)
|
||||
}
|
||||
b :: func(value int) i32 {
|
||||
return a(value)
|
||||
}
|
||||
main :: func() void {
|
||||
_ = a(1)
|
||||
}
|
||||
`
|
||||
source_file := source.Source{path="test.bro", text=text}
|
||||
diagnostics := source.init_diagnostics(&source_file)
|
||||
defer source.destroy_diagnostics(&diagnostics)
|
||||
stream := lexer.lex(&source_file, &diagnostics)
|
||||
defer delete(stream.items)
|
||||
ast_module := parser.parse(&stream, &diagnostics)
|
||||
defer ast.destroy_module(&ast_module)
|
||||
hir_module := checker.check(&ast_module, &diagnostics)
|
||||
defer hir.destroy_module(&hir_module)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect_value(t, len(hir_module.functions), 3)
|
||||
}
|
||||
|
||||
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_file("examples/prototype.bro", output)
|
||||
testing.expect_value(t, status, 0)
|
||||
state := run_executable(output)
|
||||
testing.expect_value(t, state.exit_code, 0)
|
||||
}
|
||||
|
||||
@(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_file("examples/constant_fold.bro", 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_file("examples/invalid_unused_global.bro", 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_file("examples/invalid_used_global.bro", 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_file("examples/main_int.bro", 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_file("examples/main_i32.bro", 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_file("examples/invalid_transitive_unused_global.bro", 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_file("examples/overflow.bro", output)
|
||||
testing.expect_value(t, status, 0)
|
||||
state := run_executable(output)
|
||||
testing.expect(t, !state.success)
|
||||
}
|
||||
|
||||
@(test)
|
||||
constant_that_does_not_fit_context_produces_trap_executable :: proc(t: ^testing.T) {
|
||||
output := "/tmp/brolang-test-constant-context-error"
|
||||
defer _ = os.remove(output)
|
||||
status := compiler_core.compile_file("examples/constant_context_error.bro", 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_file("examples/constant_i64_overflow.bro", 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}\n"
|
||||
source_file := source.Source{path="test.bro", text=text}
|
||||
diagnostics := source.init_diagnostics(&source_file)
|
||||
defer source.destroy_diagnostics(&diagnostics)
|
||||
stream := lexer.lex(&source_file, &diagnostics)
|
||||
defer delete(stream.items)
|
||||
module := parser.parse(&stream, &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_file("examples/missing_main.bro", 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)
|
||||
mutable_local_reassignment_uses_runtime_storage :: proc(t: ^testing.T) {
|
||||
output := "/tmp/brolang-test-mutable"
|
||||
defer _ = os.remove(output)
|
||||
status := compiler_core.compile_file("examples/mutable_local.bro", 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_file("examples/narrowing_error.bro", 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_file("examples/runtime_global.bro", 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_file("examples/cycle_unused.bro", 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_file("examples/cycle_used.bro", 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_file("examples/malformed_typed_recovery.bro", output)
|
||||
testing.expect_value(t, status, 1)
|
||||
state := run_executable(output)
|
||||
testing.expect(t, !state.success)
|
||||
}
|
||||
|
||||
@(test)
|
||||
unused_invalid_function_is_diagnosed_but_not_reached :: proc(t: ^testing.T) {
|
||||
output := "/tmp/brolang-test-invalid-unused-function"
|
||||
defer _ = os.remove(output)
|
||||
status := compiler_core.compile_file("examples/invalid_unused_function.bro", output)
|
||||
testing.expect_value(t, status, 1)
|
||||
state := run_executable(output)
|
||||
testing.expect_value(t, state.exit_code, 0)
|
||||
}
|
||||
|
||||
@(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_file("examples/function_global_unused.bro", 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_file("examples/function_global_used.bro", output)
|
||||
testing.expect_value(t, status, 1)
|
||||
state := run_executable(output)
|
||||
testing.expect(t, !state.success)
|
||||
}
|
||||
Reference in New Issue
Block a user