diff --git a/LANGUAGE.md b/LANGUAGE.md index 718df8c..cf7c9af 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -21,6 +21,8 @@ - demand-monomorphized functions - bodyful `c func` definitions using the c calling convention +- bodyless `c func` declarations with exact, globally unique external symbol names +- concrete-only foreign signatures - directory packages with merged declarations - file-local relative imports, aliases, and qualified member access @@ -28,15 +30,14 @@ - error-tolerant compilation with runtime diagnostic traps - lazy semantic checking of demanded function specializations +- demand-driven LLVM declarations for referenced foreign functions +- ordered linking of additional c sources, objects, and libraries - static, eager runtime, and deferred problematic globals ## PLANNED ### foreign functions and linking -- bodyless `c func` declarations with exact external symbol names -- concrete-only foreign signatures -- linking c sources, objects, and libraries - c variadic calls with default argument promotions - exporting brolang functions to c diff --git a/README.md b/README.md index 8000d51..60a64be 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,25 @@ odin build . -out:brolang /tmp/prototype ``` +Bodyless `c func` declarations bind exact external symbols and require concrete +types: + +```bro +foreign_add :: c func(a, b i32) i32 +``` + +Additional native inputs and libraries are passed to the final `zig cc` +invocation in command-line order: + +```sh +./brolang examples/interop/manual -o /tmp/manual \ + --link examples/interop/manual/native.c +``` + +`--link` accepts C sources, object files, and direct library paths. +`--library-path ` becomes `-L`, and `--library ` becomes +`-l`. + Compilation phases are isolated under `compiler/`: ```text @@ -41,6 +60,8 @@ Current prototype features: - Directory packages with merged declarations and file-local relative imports - Qualified imported globals and functions with package-aware symbol mangling - Demand-monomorphized Brolang and C-ABI functions +- Bodyless concrete C function declarations with exact external symbol names +- Ordered linking of additional C sources, objects, and libraries - Checked signed addition - Static, eager runtime, and deferred problematic globals - Runtime diagnostics followed by `llvm.trap` diff --git a/TODO.md b/TODO.md index 0866eb6..1343bc4 100644 --- a/TODO.md +++ b/TODO.md @@ -4,16 +4,7 @@ # milestones -1. manual c function interop - - separate calling convention, implementation, linkage, and link name - - allow bodyless `c func` declarations with exact external symbol names - - require concrete types in foreign signatures; inferred `int` is invalid - - emit LLVM `declare` for referenced foreign functions - - compile and link additional c sources, object files, and libraries through CLI options - - preserve bodyful, mangled, demand-monomorphized `c func` behavior - - verify end-to-end with an integer-only c function - -2. interop type foundation +1. interop type foundation - unsigned integers, floats, and target-dependent c scalar types - keep binding mutability (`::` / `=`) separate from element or pointee mutability (`mut`) - arrays and indexing @@ -36,7 +27,7 @@ - `Some :: c struct`: opaque c-layout struct - defer passing c structs by value until target ABI classification exists -3. restricted c header imports +2. restricted c header imports - treat an imported header as a synthetic, file-local package namespace - `import "relative/path/to/header.h"` - `other :: import "relative/path/to/header.h"` @@ -46,13 +37,13 @@ - diagnose unsupported declarations when referenced - research libclang's c API behind a replaceable c importer boundary -4. c variadic calls +3. c variadic calls - represent c variadics as a fixed parameter count plus a variadic flag - apply c default argument promotions at call sites - emit LLVM c-variadic declarations and calls - keep native brolang variadics and tuple design separate -5. advanced c interop +4. advanced c interop - by-value records and unions - function pointers and callbacks - external variables diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index b724c7b..ef51002 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -66,6 +66,7 @@ Function :: struct { pkg: int, file: int, c_abi: bool, + has_body: bool, params: []Param, result: Type_Syntax, body: []int, diff --git a/compiler/backend/backend.odin b/compiler/backend/backend.odin index 1efe6e3..4413e86 100644 --- a/compiler/backend/backend.odin +++ b/compiler/backend/backend.odin @@ -1,25 +1,59 @@ package backend +import "../linker" import "core:fmt" +import "core:mem" import "core:os" import "core:os/os2" +import "core:strings" -compile :: proc(llvm_path, output_path: string) -> bool { +append_owned :: proc(command: ^[dynamic]string, value: string, allocator: mem.Allocator) { + append(command, strings.clone(value, allocator)) +} + +build_command :: proc( + llvm_path, output_path: string, + link_arguments: []linker.Argument, + allocator := context.allocator, +) -> []string { + command: [dynamic]string + command.allocator = allocator + append_owned(&command, "/usr/bin/env", allocator) + append_owned(&command, "ZIG_LOCAL_CACHE_DIR=/tmp/brolang-zig-cache", allocator) + append_owned(&command, "ZIG_GLOBAL_CACHE_DIR=/tmp/brolang-zig-global-cache", allocator) + append_owned(&command, "zig", allocator) + append_owned(&command, "cc", allocator) + append_owned(&command, "-Wno-override-module", allocator) + append_owned(&command, llvm_path, allocator) + for argument in link_arguments { + switch argument.kind { + case .Input: + append_owned(&command, argument.value, allocator) + case .Library_Path: + append(&command, fmt.aprintf("-L%s", argument.value, allocator=allocator)) + case .Library: + append(&command, fmt.aprintf("-l%s", argument.value, allocator=allocator)) + } + } + append_owned(&command, "-o", allocator) + append_owned(&command, output_path, allocator) + return command[:] +} + +destroy_command :: proc(command: []string, allocator := context.allocator) { + for argument in command { + delete(argument, allocator) + } + delete(command, allocator) +} + +compile :: proc(llvm_path, output_path: string, link_arguments: []linker.Argument = nil) -> bool { pid := os2.get_pid() temporary_output := fmt.tprintf("%s.brolang-tmp-%d", output_path, pid) defer _ = os.remove(temporary_output) - command := []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", - llvm_path, - "-o", - temporary_output, - } + command := build_command(llvm_path, temporary_output, link_arguments) + defer destroy_command(command) state, stdout, stderr, err := os2.process_exec( os2.Process_Desc{command=command}, context.allocator, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index f00bec3..e09d045 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -49,19 +49,20 @@ Symbol_Index_Entry :: struct { } Checker :: struct { - ast_module: ^ast.Module, - diagnostics: ^source.Diagnostics, - symbols: ^symbol.Table, - module: hir.Module, - specs: [dynamic]Spec, - function_index: []Symbol_Index_Entry, - global_index: []Symbol_Index_Entry, - import_index: []Symbol_Index_Entry, - global_types: []types.Type, - constants: []Constant, - main_symbol: symbol.Id, - sink_symbol: symbol.Id, - allocator: mem.Allocator, + ast_module: ^ast.Module, + diagnostics: ^source.Diagnostics, + symbols: ^symbol.Table, + module: hir.Module, + specs: [dynamic]Spec, + function_index: []Symbol_Index_Entry, + global_index: []Symbol_Index_Entry, + import_index: []Symbol_Index_Entry, + global_types: []types.Type, + constants: []Constant, + template_diagnostics: []int, + main_symbol: symbol.Id, + sink_symbol: symbol.Id, + allocator: mem.Allocator, } symbol_text :: proc(checker: ^Checker, id: symbol.Id) -> string { @@ -287,7 +288,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id, file: int) { } validate_declarations :: proc(checker: ^Checker) { - for function in checker.ast_module.functions { + for function, function_id in checker.ast_module.functions { locals: [dynamic]symbol.Id locals.allocator = checker.allocator for param in function.params { @@ -308,6 +309,42 @@ validate_declarations :: proc(checker: ^Checker) { } append(&locals, param.name) } + if !function.has_body && !function.c_abi { + checker.template_diagnostics[function_id] = source.addf( + checker.diagnostics, + function.span, + "bodyless function '%s' must use 'c func'", + symbol_text(checker, function.name), + ) + } + if !function.has_body && function.c_abi { + for param in function.params { + if type_from_syntax(param.type).kind != .Concrete { + checker.template_diagnostics[function_id] = source.addf( + checker.diagnostics, + param.span, + "foreign function '%s' requires concrete parameter types", + symbol_text(checker, function.name), + ) + } + } + result := type_from_syntax(function.result) + if result.kind != .Concrete && result.kind != .Void { + checker.template_diagnostics[function_id] = source.addf( + checker.diagnostics, + function.span, + "foreign function '%s' requires a concrete or void result type", + symbol_text(checker, function.name), + ) + } + if function.pkg == 0 && function.name == checker.main_symbol { + checker.template_diagnostics[function_id] = source.add( + checker.diagnostics, + function.span, + "main must have a body", + ) + } + } for statement_id in function.body { statement := checker.ast_module.statements[statement_id] switch statement.kind { @@ -318,6 +355,23 @@ validate_declarations :: proc(checker: ^Checker) { } delete(locals) } + for function, function_id in checker.ast_module.functions { + if function.has_body || !function.c_abi { + continue + } + for other, other_id in checker.ast_module.functions { + if other_id == function_id || other.has_body || !other.c_abi || other.name != function.name { + continue + } + checker.template_diagnostics[function_id] = source.addf( + checker.diagnostics, + function.span, + "duplicate foreign symbol '%s'", + symbol_text(checker, function.name), + ) + break + } + } } find_infer_local :: proc(locals: []Infer_Local, name: symbol.Id) -> types.Type { @@ -437,6 +491,13 @@ infer_expr :: proc(checker: ^Checker, expr_id: int, locals: []Infer_Local, pkg : if template < 0 { return types.INVALID } + if checker.template_diagnostics[template] >= 0 { + declared := type_from_syntax(checker.ast_module.functions[template].result) + if declared.kind == .Concrete || declared.kind == .Void { + return declared + } + return types.INVALID + } args := make([]types.Type, len(expr.args), checker.allocator) for arg, index in expr.args { args[index] = infer_expr(checker, arg, locals, pkg, file) @@ -794,6 +855,9 @@ build_expr :: proc( id := add_call_resolution_diagnostic(checker, expr, target_pkg) return invalid_hir_expr(checker, expr.span, id) } + if checker.template_diagnostics[template] >= 0 { + return invalid_hir_expr(checker, expr.span, checker.template_diagnostics[template]) + } if len(expr.args) != len(checker.ast_module.functions[template].params) { id := source.addf( checker.diagnostics, @@ -873,6 +937,9 @@ make_link_name :: proc(checker: ^Checker, spec_id: int) -> string { if function.pkg == 0 && function.name == checker.main_symbol { return fmt.aprintf("main", allocator = checker.allocator) } + if !function.has_body && function.c_abi { + return fmt.aprintf("%s", symbol_text(checker, function.name), allocator = checker.allocator) + } builder := strings.builder_make(checker.allocator) defer strings.builder_destroy(&builder) strings.write_string(&builder, "bro_c__" if function.c_abi else "bro__") @@ -940,7 +1007,31 @@ build_function :: proc(checker: ^Checker, spec_id: int) { append(¶ms, local_id) } - problematic := signature_diagnostic >= 0 + problematic := signature_diagnostic >= 0 || checker.template_diagnostics[spec.template] >= 0 + if !function.has_body { + append( + &checker.module.functions, + hir.Function { + name = function.name, + link_name = make_link_name(checker, spec_id), + calling_convention = .C if function.c_abi else .Brolang, + implementation = .Declaration, + linkage = .External if function.c_abi else .Internal, + is_main = function.pkg == 0 && function.name == checker.main_symbol, + params = params[:], + result = spec.result, + locals = hir_locals[:], + body = body[:], + direct_global_reads = global_reads[:], + calls = calls[:], + problematic = problematic, + diagnostic = checker.template_diagnostics[spec.template], + }, + ) + delete(locals) + return + } + has_return := false if signature_diagnostic >= 0 { append(&body, len(checker.module.statements)) @@ -1288,7 +1379,9 @@ build_function :: proc(checker: ^Checker, spec_id: int) { hir.Function { name = function.name, link_name = make_link_name(checker, spec_id), - c_abi = function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol), + calling_convention = .C if function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol) else .Brolang, + implementation = .Definition, + linkage = .External if function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol) else .Internal, is_main = function.pkg == 0 && function.name == checker.main_symbol, params = params[:], result = spec.result, @@ -1547,7 +1640,9 @@ synthesize_trap_main :: proc(checker: ^Checker) { hir.Function { name = checker.main_symbol, link_name = fmt.aprintf("main", allocator = checker.allocator), - c_abi = true, + calling_convention = .C, + implementation = .Definition, + linkage = .External, is_main = true, result = types.VOID, body = body, @@ -1566,6 +1661,9 @@ replace_main_with_trap :: proc(checker: ^Checker, diagnostic: int) { delete(function.body, checker.allocator) function.params = nil function.result = types.VOID + function.calling_convention = .C + function.implementation = .Definition + function.linkage = .External function.problematic = true function.diagnostic = diagnostic statement_id := len(checker.module.statements) @@ -1605,6 +1703,10 @@ check :: proc( build_symbol_indexes(&checker) checker.global_types = make([]types.Type, len(ast_module.globals), allocator) checker.constants = make([]Constant, len(ast_module.exprs), allocator) + checker.template_diagnostics = make([]int, len(ast_module.functions), allocator) + for &diagnostic in checker.template_diagnostics { + diagnostic = -1 + } defer { for spec in checker.specs { delete(spec.args, allocator) @@ -1615,6 +1717,7 @@ check :: proc( delete(checker.import_index, allocator) delete(checker.global_types, allocator) delete(checker.constants, allocator) + delete(checker.template_diagnostics, allocator) } for function, index in ast_module.functions { @@ -1658,13 +1761,17 @@ check :: proc( } else { template := ast_module.functions[main_template] if main_declarations != 1 || + !template.has_body || len(template.params) != 0 || !(template.result == .Void || template.result == .I32 || template.result == .Int) { - id := source.add( - diagnostics, - template.span, - "main must be unique, take no parameters, and return void, i32, or int", - ) + id := checker.template_diagnostics[main_template] + if id < 0 { + id = source.add( + diagnostics, + template.span, + "main must be unique, have a body, take no parameters, and return void, i32, or int", + ) + } replace_main_with_trap(&checker, id) } } diff --git a/compiler/compiler.odin b/compiler/compiler.odin index 3ede0f6..ac5dafb 100644 --- a/compiler/compiler.odin +++ b/compiler/compiler.odin @@ -3,6 +3,7 @@ package compiler import "./backend" import "./checker" import "./llvm" +import "./linker" import "./loader" import "./lower" import "./opt" @@ -13,7 +14,7 @@ import vmem "core:mem/virtual" import "core:os" import "core:os/os2" -compile_package :: proc(input_path, output_path: string) -> int { +compile_package :: proc(input_path, output_path: string, link_arguments: []linker.Argument = nil) -> int { sources := source.init_store() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) @@ -76,7 +77,7 @@ compile_package :: proc(input_path, output_path: string) -> int { } source.print_all(&diagnostics) - if !backend.compile(llvm_path, output_path) { + if !backend.compile(llvm_path, output_path, link_arguments) { return 2 } if len(diagnostics.items) > 0 { diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 8d88313..76d859f 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -7,6 +7,21 @@ import "core:mem" INVALID_ID :: -1 +Calling_Convention :: enum { + Brolang, + C, +} + +Implementation :: enum { + Definition, + Declaration, +} + +Linkage :: enum { + Internal, + External, +} + Expr_Kind :: enum { Invalid, Integer, @@ -56,7 +71,9 @@ Stmt :: struct { Function :: struct { name: symbol.Id, link_name: string, - c_abi: bool, + calling_convention: Calling_Convention, + implementation: Implementation, + linkage: Linkage, is_main: bool, params: []int, result: types.Type, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index 012af72..a05b921 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -7,6 +7,21 @@ import "core:mem" INVALID_ID :: -1 +Calling_Convention :: enum { + Brolang, + C, +} + +Implementation :: enum { + Definition, + Declaration, +} + +Linkage :: enum { + Internal, + External, +} + Opcode :: enum { Param, Const, @@ -35,13 +50,15 @@ Instruction :: struct { } Function :: struct { - link_name: string, - c_abi: bool, - is_main: bool, - param_types: []types.Type, - result: types.Type, - instructions: []Instruction, - problematic: bool, + link_name: string, + calling_convention: Calling_Convention, + implementation: Implementation, + linkage: Linkage, + is_main: bool, + param_types: []types.Type, + result: types.Type, + instructions: []Instruction, + problematic: bool, } Global :: struct { diff --git a/compiler/linker/linker.odin b/compiler/linker/linker.odin new file mode 100644 index 0000000..5d6c602 --- /dev/null +++ b/compiler/linker/linker.odin @@ -0,0 +1,12 @@ +package linker + +Kind :: enum { + Input, + Library_Path, + Library, +} + +Argument :: struct { + kind: Kind, + value: string, +} diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 83e3040..fad4596 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -202,7 +202,7 @@ emit_instruction_stream :: proc( strings.write_string(&emitter.builder, " ") } strings.write_string(&emitter.builder, "call ") - if !target.c_abi { + if target.calling_convention == .Brolang { strings.write_string(&emitter.builder, "fastcc ") } fmt.sbprintf(&emitter.builder, "%s @%s(", function_result_type(target), target.link_name) @@ -321,16 +321,31 @@ emit_constructor :: proc(emitter: ^Emitter) { emit_functions :: proc(emitter: ^Emitter) { for function in emitter.module.functions { - strings.write_string(&emitter.builder, "define ") - if !function.c_abi { - strings.write_string(&emitter.builder, "internal fastcc ") + if function.implementation == .Declaration { + strings.write_string(&emitter.builder, "declare ") + } else { + strings.write_string(&emitter.builder, "define ") + if function.linkage == .Internal { + strings.write_string(&emitter.builder, "internal ") + } + } + if function.calling_convention == .Brolang { + strings.write_string(&emitter.builder, "fastcc ") } fmt.sbprintf(&emitter.builder, "%s @%s(", function_result_type(function), function.link_name) for param_type, index in function.param_types { if index > 0 { strings.write_string(&emitter.builder, ", ") } - fmt.sbprintf(&emitter.builder, "%s %%v%d", llvm_type(param_type), index) + if function.implementation == .Declaration { + fmt.sbprintf(&emitter.builder, "%s", llvm_type(param_type)) + } else { + fmt.sbprintf(&emitter.builder, "%s %%v%d", llvm_type(param_type), index) + } + } + if function.implementation == .Declaration { + strings.write_string(&emitter.builder, ")\n\n") + continue } strings.write_string(&emitter.builder, ") {\nentry:\n") _ = emit_instruction_stream(emitter, function.instructions, function) diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index f974115..8c00600 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -343,11 +343,13 @@ lower :: proc(hir_module: ^hir.Module, allocator := context.allocator) -> ir.Mod } append(&module.functions, ir.Function{ link_name=fmt.aprintf("%s", function.link_name, allocator=allocator), - c_abi=function.c_abi, + calling_convention=.C if function.calling_convention == .C else .Brolang, + implementation=.Declaration if function.implementation == .Declaration else .Definition, + linkage=.External if function.linkage == .External else .Internal, is_main=function.is_main, param_types=param_types, result=function.result, - instructions=lower_body(hir_module, function, allocator), + instructions=nil if function.implementation == .Declaration else lower_body(hir_module, function, allocator), problematic=function.problematic, }) } diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 1562f23..5d964ab 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -385,10 +385,29 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { } skip_newlines(parser) result := parse_type(parser) - skip_newlines(parser) - if _, ok := allow(parser, .Left_Brace); !ok { - source.add(parser.diagnostics, current(parser).span, "expected '{' before function body") + end := previous(parser) + ended_by_newline := current(parser).kind == .Newline + if current(parser).kind == .Newline { + skip_newlines(parser) } + if current(parser).kind != .Left_Brace { + if !ended_by_newline && current(parser).kind != .Eof { + _ = finish_statement(parser) + } + append(&parser.module.functions, ast.Function{ + span=span_from(name.span, end.span), + name=name.symbol, + pkg=parser.pkg, + file=parser.file, + c_abi=c_abi, + has_body=false, + params=params, + result=result, + diagnostic=-1, + }) + return + } + advance(parser) body: [dynamic]int body.allocator = parser.module.allocator @@ -406,7 +425,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { append(&body, statement_id) } } - end := current(parser) + end = current(parser) if _, ok := allow(parser, .Right_Brace); !ok { source.add(parser.diagnostics, current(parser).span, "expected '}' after function body") end = func_token @@ -417,6 +436,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { pkg=parser.pkg, file=parser.file, c_abi=c_abi, + has_body=true, params=params, result=result, body=body[:], diff --git a/compiler_tests.odin b/compiler_tests.odin index 8c69896..5714af3 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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" diff --git a/examples/interop/manual/main.bro b/examples/interop/manual/main.bro new file mode 100644 index 0000000..9287eec --- /dev/null +++ b/examples/interop/manual/main.bro @@ -0,0 +1,5 @@ +foreign_add :: c func(a, b i32) i32 + +main :: func() i32 { + return foreign_add(20, 22) +} diff --git a/examples/interop/manual/native.c b/examples/interop/manual/native.c new file mode 100644 index 0000000..9b41363 --- /dev/null +++ b/examples/interop/manual/native.c @@ -0,0 +1,3 @@ +int foreign_add(int a, int b) { + return a + b; +} diff --git a/examples/packages/foreign_duplicate/app/main.bro b/examples/packages/foreign_duplicate/app/main.bro new file mode 100644 index 0000000..e978b67 --- /dev/null +++ b/examples/packages/foreign_duplicate/app/main.bro @@ -0,0 +1,7 @@ +import "../left" +import "../right" + +main :: func() void { + _ = left.same() + _ = right.same() +} diff --git a/examples/packages/foreign_duplicate/left/left.bro b/examples/packages/foreign_duplicate/left/left.bro new file mode 100644 index 0000000..7eb4b87 --- /dev/null +++ b/examples/packages/foreign_duplicate/left/left.bro @@ -0,0 +1 @@ +same :: c func() i32 diff --git a/examples/packages/foreign_duplicate/right/right.bro b/examples/packages/foreign_duplicate/right/right.bro new file mode 100644 index 0000000..7eb4b87 --- /dev/null +++ b/examples/packages/foreign_duplicate/right/right.bro @@ -0,0 +1 @@ +same :: c func() i32 diff --git a/main.odin b/main.odin index 5daf450..7800563 100644 --- a/main.odin +++ b/main.odin @@ -1,15 +1,75 @@ package main import "./compiler" +import "./compiler/linker" import "core:fmt" import "core:os/os2" +Cli_Options :: struct { + input_path: string, + output_path: string, + link_arguments: []linker.Argument, +} + +parse_cli_args :: proc(args: []string, allocator := context.allocator) -> (Cli_Options, bool) { + if len(args) < 4 { + return {}, false + } + options := Cli_Options{input_path=args[1]} + link_arguments: [dynamic]linker.Argument + link_arguments.allocator = allocator + output_set := false + cursor := 2 + for cursor < len(args) { + option := args[cursor] + cursor += 1 + if cursor >= len(args) { + delete(link_arguments) + return {}, false + } + value := args[cursor] + cursor += 1 + switch option { + case "-o": + if output_set { + delete(link_arguments) + return {}, false + } + output_set = true + options.output_path = value + case "--link": + append(&link_arguments, linker.Argument{kind=.Input, value=value}) + case "--library-path": + append(&link_arguments, linker.Argument{kind=.Library_Path, value=value}) + case "--library": + append(&link_arguments, linker.Argument{kind=.Library, value=value}) + case: + delete(link_arguments) + return {}, false + } + } + if !output_set || len(options.output_path) == 0 { + delete(link_arguments) + return {}, false + } + options.link_arguments = link_arguments[:] + return options, true +} + +print_usage :: proc() { + fmt.eprintln( + "usage: brolang -o [--link | --library-path | --library ]...", + ) +} + main :: proc() { - if len(os2.args) != 4 || os2.args[2] != "-o" { - fmt.eprintln("usage: brolang -o ") + options, valid := parse_cli_args(os2.args) + if !valid { + print_usage() os2.exit(2) } - status := compiler.compile_package(os2.args[1], os2.args[3]) + defer delete(options.link_arguments) + status := compiler.compile_package(options.input_path, options.output_path, options.link_arguments) if status != 0 { os2.exit(status) }