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)
}