manual c function interop
This commit is contained in:
@@ -7,6 +7,7 @@ import "./compiler/checker"
|
||||
import "./compiler/hir"
|
||||
import "./compiler/ir"
|
||||
import "./compiler/lexer"
|
||||
import "./compiler/linker"
|
||||
import "./compiler/loader"
|
||||
import "./compiler/llvm"
|
||||
import "./compiler/lower"
|
||||
@@ -143,6 +144,76 @@ main :: func() void { _ = give() }
|
||||
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)
|
||||
cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T) {
|
||||
options, valid := parse_cli_args([]string{
|
||||
"brolang",
|
||||
"app",
|
||||
"--link",
|
||||
"native.c",
|
||||
"-o",
|
||||
"app.out",
|
||||
"--library-path",
|
||||
"vendor/lib",
|
||||
"--library",
|
||||
"thing",
|
||||
"--link",
|
||||
"helper.o",
|
||||
})
|
||||
defer delete(options.link_arguments)
|
||||
|
||||
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")
|
||||
|
||||
_, 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"})
|
||||
testing.expect(t, !unknown_valid)
|
||||
testing.expect(t, !incomplete_valid)
|
||||
testing.expect(t, !duplicate_output_valid)
|
||||
testing.expect(t, !duplicate_empty_output_valid)
|
||||
}
|
||||
|
||||
@(test)
|
||||
parser_accepts_bare_and_aliased_imports :: proc(t: ^testing.T) {
|
||||
text := `import
|
||||
@@ -319,6 +390,139 @@ main :: func() void {
|
||||
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)
|
||||
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 {
|
||||
@@ -604,6 +808,43 @@ main :: func() void {}
|
||||
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}},
|
||||
@@ -624,6 +865,68 @@ valid_program_compiles_and_runs :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, state.exit_code, 0)
|
||||
}
|
||||
|
||||
@(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)
|
||||
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"
|
||||
@@ -762,6 +1065,38 @@ backend_failure_preserves_existing_output :: proc(t: ^testing.T) {
|
||||
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"},
|
||||
}
|
||||
command := backend.build_command("module.ll", "program", arguments)
|
||||
defer backend.destroy_command(command)
|
||||
|
||||
expected := []string{
|
||||
"/usr/bin/env",
|
||||
"ZIG_LOCAL_CACHE_DIR=/tmp/brolang-zig-cache",
|
||||
"ZIG_GLOBAL_CACHE_DIR=/tmp/brolang-zig-global-cache",
|
||||
"zig",
|
||||
"cc",
|
||||
"-Wno-override-module",
|
||||
"module.ll",
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
mutable_local_reassignment_uses_runtime_storage :: proc(t: ^testing.T) {
|
||||
output := "/tmp/brolang-test-mutable"
|
||||
|
||||
Reference in New Issue
Block a user