manual c function interop

This commit is contained in:
2026-06-11 22:46:12 +02:00
parent 2a79010a57
commit a5ceb727c1
20 changed files with 725 additions and 74 deletions
+335
View File
@@ -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"