From 638ca57f5c184fa063d72da9be711af0b49fdd26 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 14 Jun 2026 14:42:38 +0200 Subject: [PATCH] restricted c header imports --- LANGUAGE.md | 11 +- README.md | 27 +- TODO.md | 9 +- compiler/ast/ast.odin | 21 + compiler/backend/backend.odin | 12 +- compiler/checker/checker.odin | 116 ++++- compiler/cimport/cimport.odin | 140 ++++++ compiler/cimport/libclang.odin | 464 +++++++++++++++++++ compiler/compiler.odin | 6 +- compiler/hir/hir.odin | 1 + compiler/ir/ir.odin | 1 + compiler/llvm/llvm.odin | 23 +- compiler/loader/loader.odin | 239 +++++++++- compiler/lower/lower.odin | 5 +- compiler/types/types.odin | 17 +- compiler_tests.odin | 174 ++++++- examples/interop/header/app/main.bro | 13 + examples/interop/header/include/child.h | 6 + examples/interop/header/include/native.h | 40 ++ examples/interop/header/native.c | 33 ++ examples/interop/header_cache/main.bro | 4 + examples/interop/header_duplicate/main.bro | 7 + examples/interop/header_unsupported/main.bro | 15 + main.odin | 38 +- 24 files changed, 1377 insertions(+), 45 deletions(-) create mode 100644 compiler/cimport/cimport.odin create mode 100644 compiler/cimport/libclang.odin create mode 100644 examples/interop/header/app/main.bro create mode 100644 examples/interop/header/include/child.h create mode 100644 examples/interop/header/include/native.h create mode 100644 examples/interop/header/native.c create mode 100644 examples/interop/header_cache/main.bro create mode 100644 examples/interop/header_duplicate/main.bro create mode 100644 examples/interop/header_unsupported/main.bro diff --git a/LANGUAGE.md b/LANGUAGE.md index d6555f5..d9c813c 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -32,6 +32,9 @@ - Apple Silicon C ABI scalar and pointer lowering, including narrow integer extension attributes - directory packages with merged declarations - file-local relative imports, aliases, and qualified member access +- relative `.h` imports as synthetic package namespaces +- transitive external C function prototypes, typedef chains, C scalars, and pointers to opaque C records +- reference-time diagnostics for unsupported imported C declarations ### compiler behavior @@ -39,6 +42,8 @@ - lazy semantic checking of demanded function specializations - demand-driven LLVM declarations for referenced foreign functions - ordered linking of additional c sources, objects, and libraries +- replaceable dynamically loaded libclang C-import backend +- per-compilation C-header import caching by canonical path, target, ordered include paths, and ordered defines - static, eager runtime, and deferred problematic globals ## PLANNED @@ -53,9 +58,7 @@ - tuples and native variadic functions - C unions, C enums, and by-value C record ABI lowering -### c imports +### advanced c imports -- c headers imported as synthetic package namespaces -- typedefs, enums, opaque records, and external variables -- function pointers, callbacks, macros, and static inline functions +- C enums, external variables, function pointers, callbacks, macros, and static inline functions - target-specific by-value C record and union ABI lowering diff --git a/README.md b/README.md index b4b6ed7..c7e744f 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,28 @@ invocation in command-line order: ```sh ./build/brolang examples/interop/manual -o build/manual \ - --link examples/interop/manual/native.c + --c-link examples/interop/manual/native.c ``` -`--link` accepts C sources, object files, and direct library paths. -`--library-path ` becomes `-L`, and `--library ` becomes -`-l`. +`--c-link` accepts C sources, object files, archives, and direct library paths. +`--c-library-path ` becomes `-L`, and `--c-library ` becomes +`-l`. `--c-include-path ` and `--c-define ` configure +C preprocessing. + +Relative `.h` imports create synthetic package namespaces backed by libclang: + +```bro +native :: import "../include/native.h" + +main :: func() void { + _ = native.imported_add(20, 22) +} +``` + +Header imports expose supported external functions, typedefs, C scalars, and +pointers to opaque records. They never add linker inputs; implementations must +still be supplied explicitly with the C-prefixed linking options. Set +`BROLANG_LIBCLANG_PATH` when libclang is not installed in a standard location. Compilation phases are isolated under `compiler/`: @@ -63,10 +79,11 @@ Current prototype features: - Explicit `.ptr`/`.len`, slicing, postfix pointer dereference and optional unwrap, and keyed struct literals - Contextual integer constants and compile-time folding of addition and unary negation trees - Directory packages with merged declarations and file-local relative imports +- Relative C header imports as synthetic package namespaces - 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 +- Ordered linking of additional C sources, objects, archives, and libraries - Checked signed addition and unary negation - Static, eager runtime, and deferred problematic globals - Runtime diagnostics followed by `llvm.trap` diff --git a/TODO.md b/TODO.md index dc17cc6..bd00fd1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,4 @@ -# "quick"/"easy" fixes +# "quick" / "easy" fixes - for global initialization cycles, report also starting and ending lines @@ -29,15 +29,14 @@ - `Some :: c_struct`: opaque c-layout struct - defer passing c structs by value until target ABI classification exists -2. restricted c header imports +2. restricted c header imports (implemented) - treat an imported header as a synthetic, file-local package namespace - - `import "relative/path/to/header.h"` - - `other :: import "relative/path/to/header.h"` + - `native :: import "relative/path/to/header.h"` - import functions, typedefs, scalar types, and pointers to opaque records - keep implementation linking separate from header imports - cache imports by canonical header path and target/include/define configuration - diagnose unsupported declarations when referenced - - research libclang's c API behind a replaceable c importer boundary + - dynamically load libclang behind a replaceable c importer boundary 3. c variadic calls - represent c variadics as a fixed parameter count plus a variadic flag diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index bbbe126..311153a 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -128,10 +128,12 @@ Function :: struct { pkg: Package_Id, file: File_Id, c_abi: bool, + imported: bool, has_body: bool, params: []Param, result: Type_Syntax, body: []Stmt_Id, + unsupported_reason: string, diagnostic: source.Diagnostic_Id, } @@ -167,6 +169,18 @@ Package :: struct { path: string, name: symbol.Id, available: bool, + kind: Package_Kind, +} + +Package_Kind :: enum u8 { + Native, + C_Header, +} + +Unsupported :: struct { + pkg: Package_Id, + name: symbol.Id, + reason: string, } Module :: struct { @@ -177,6 +191,7 @@ Module :: struct { imports: [dynamic]Import, files: [dynamic]File, packages: [dynamic]Package, + unsupported: [dynamic]Unsupported, strings: [dynamic]string, type_store: types.Store, allocator: mem.Allocator, @@ -193,6 +208,7 @@ init_module :: proc(allocator := context.allocator) -> Module { module.imports.allocator = allocator module.files.allocator = allocator module.packages.allocator = allocator + module.unsupported.allocator = allocator module.strings.allocator = allocator return module } @@ -204,6 +220,7 @@ destroy_module :: proc(module: ^Module) { for function in module.functions { delete(function.params, module.allocator) delete(function.body, module.allocator) + delete(function.unsupported_reason, module.allocator) } for import_item in module.imports { delete(import_item.path, module.allocator) @@ -211,6 +228,9 @@ destroy_module :: proc(module: ^Module) { for pkg in module.packages { delete(pkg.path, module.allocator) } + for item in module.unsupported { + delete(item.reason, module.allocator) + } for value in module.strings { delete(value, module.allocator) } @@ -221,6 +241,7 @@ destroy_module :: proc(module: ^Module) { delete(module.imports) delete(module.files) delete(module.packages) + delete(module.unsupported) delete(module.strings) types.destroy_store(&module.type_store) } diff --git a/compiler/backend/backend.odin b/compiler/backend/backend.odin index e3b5805..c1cb66a 100644 --- a/compiler/backend/backend.odin +++ b/compiler/backend/backend.odin @@ -2,6 +2,7 @@ package backend import "../linker" import "../target" +import "../cimport" import "core:fmt" import "core:mem" import "core:os" @@ -16,6 +17,7 @@ build_command :: proc( llvm_path, output_path: string, link_arguments: []linker.Argument, selected := target.DEFAULT, + c_options := cimport.Options{}, allocator := context.allocator, ) -> []string { command: [dynamic]string @@ -26,7 +28,14 @@ build_command :: proc( append_owned(&command, "-target", allocator) append_owned(&command, target.name(selected), allocator) append_owned(&command, "-Wno-override-module", allocator) + append_owned(&command, "-Wno-unused-command-line-argument", allocator) append_owned(&command, llvm_path, allocator) + for path in c_options.include_paths { + append(&command, fmt.aprintf("-I%s", path, allocator=allocator)) + } + for define in c_options.defines { + append(&command, fmt.aprintf("-D%s", define, allocator=allocator)) + } for argument in link_arguments { switch argument.kind { case .Input: @@ -53,12 +62,13 @@ compile :: proc( llvm_path, output_path: string, link_arguments: []linker.Argument = nil, selected := target.DEFAULT, + c_options := cimport.Options{}, ) -> bool { pid := os2.get_pid() temporary_output := fmt.tprintf("%s.brolang-tmp-%d", output_path, pid) defer _ = os.remove(temporary_output) - command := build_command(llvm_path, temporary_output, link_arguments, selected) + command := build_command(llvm_path, temporary_output, link_arguments, selected, c_options) defer destroy_command(command) state, stdout, stderr, err := os2.process_exec( os2.Process_Desc{command=command}, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 0b9e5e2..da3f722 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -409,6 +409,62 @@ add_call_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target return source.addf(checker.diagnostics, expr.span, "unresolved function '%s'", symbol_text(checker, expr.name)) } +find_unsupported :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id) -> (ast.Unsupported, bool) { + for item in checker.ast_module.unsupported { + if item.pkg == pkg && item.name == name { + return item, true + } + } + return {}, false +} + +add_unsupported_diagnostic :: proc(checker: ^Checker, span: source.Span, pkg: ast.Package_Id, name: symbol.Id) -> source.Diagnostic_Id { + if item, ok := find_unsupported(checker, pkg, name); ok { + return source.addf( + checker.diagnostics, + span, + "C declaration '%s' is unavailable: %s", + symbol_text(checker, name), + item.reason, + ) + } + return source.INVALID_DIAGNOSTIC +} + +add_unsupported_type_diagnostic :: proc( + checker: ^Checker, + span: source.Span, + value: types.Type, + depth := 0, +) -> source.Diagnostic_Id { + if depth > 64 { + return source.INVALID_DIAGNOSTIC + } + item, ok := types.node(&checker.module.types, value) + if !ok { + return source.INVALID_DIAGNOSTIC + } + if item.kind == .Alias { + return add_unsupported_diagnostic(checker, span, ast.Package_Id(item.pkg), symbol.Id(item.name)) + } + if types.is_valid(item.child) { + return add_unsupported_type_diagnostic(checker, span, item.child, depth+1) + } + return source.INVALID_DIAGNOSTIC +} + +function_signatures_equal :: proc(left, right: ast.Function) -> bool { + if left.result != right.result || len(left.params) != len(right.params) { + return false + } + for param, index in left.params { + if param.type != right.params[index].type { + return false + } + } + return true +} + contains_name :: proc(names: []symbol.Id, name: symbol.Id) -> bool { for existing in names { if existing == name { @@ -454,9 +510,17 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as validate_declarations :: proc(checker: ^Checker) { for function, function_id in checker.ast_module.functions { + if len(function.unsupported_reason) > 0 { + continue + } locals: [dynamic]symbol.Id locals.allocator = checker.allocator for param in function.params { + if diagnostic := add_unsupported_type_diagnostic(checker, param.span, type_from_syntax(param.type)); + diagnostic != source.INVALID_DIAGNOSTIC { + checker.template_diagnostics[function_id] = diagnostic + continue + } if param.type == types.VOID { source.add( checker.diagnostics, @@ -482,6 +546,10 @@ validate_declarations :: proc(checker: ^Checker) { ) } } + if diagnostic := add_unsupported_type_diagnostic(checker, function.span, type_from_syntax(function.result)); + diagnostic != source.INVALID_DIAGNOSTIC { + checker.template_diagnostics[function_id] = diagnostic + } if types.contains_c_struct_by_value(type_from_syntax(function.result), &checker.module.types) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, @@ -500,6 +568,10 @@ validate_declarations :: proc(checker: ^Checker) { } if !function.has_body && function.c_abi { for param in function.params { + if add_unsupported_type_diagnostic(checker, param.span, type_from_syntax(param.type)) != + source.INVALID_DIAGNOSTIC { + continue + } if !types.is_c_signature_type(type_from_syntax(param.type), &checker.module.types) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, @@ -510,7 +582,8 @@ validate_declarations :: proc(checker: ^Checker) { } } result := type_from_syntax(function.result) - if !types.is_c_signature_type(result, &checker.module.types, true) { + if add_unsupported_type_diagnostic(checker, function.span, result) == source.INVALID_DIAGNOSTIC && + !types.is_c_signature_type(result, &checker.module.types, true) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, @@ -547,6 +620,9 @@ validate_declarations :: proc(checker: ^Checker) { if other_id == function_id || other.has_body || !other.c_abi || other.name != function.name { continue } + if function.imported && other.imported && function_signatures_equal(function, other) { + continue + } checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, @@ -901,6 +977,11 @@ infer_expr :: proc( _ = pop(&stack) continue } + if len(checker.ast_module.functions[template].unsupported_reason) > 0 { + last = types.INVALID + _ = pop(&stack) + continue + } if checker.template_diagnostics[template] != source.INVALID_DIAGNOSTIC { declared := type_from_syntax(checker.ast_module.functions[template].result) last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID @@ -1185,8 +1266,15 @@ coerce_expr :: proc( return expr_id } if types.can_weaken_pointer(actual, expected, &checker.module.types) { - checker.module.exprs[expr_id].type = expected - return expr_id + return add_hir_expr(checker, hir.Expr{ + kind=.Weaken_Pointer, + span=span, + type=expected, + left=expr_id, + target=hir.INVALID_REF, + right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) } if types.is_optional(expected, &checker.module.types) { child := types.child_type(expected, &checker.module.types) @@ -1754,7 +1842,10 @@ build_expr :: proc( target=hir.global_ref(hir_global), left = hir.INVALID_EXPR, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, }) } else { - id := add_name_resolution_diagnostic(checker, expr, target_pkg) + id := add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name) + if id == source.INVALID_DIAGNOSTIC { + id = add_name_resolution_diagnostic(checker, expr, target_pkg) + } last = invalid_hir_expr(checker, expr.span, id) } } @@ -1775,7 +1866,22 @@ build_expr :: proc( } template := find_template(checker, expr.name, target_pkg) if template == ast.INVALID_FUNCTION { - id := add_call_resolution_diagnostic(checker, expr, target_pkg) + id := add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name) + if id == source.INVALID_DIAGNOSTIC { + id = add_call_resolution_diagnostic(checker, expr, target_pkg) + } + last = invalid_hir_expr(checker, expr.span, id) + _ = pop(&stack) + continue + } + if len(checker.ast_module.functions[template].unsupported_reason) > 0 { + id := source.addf( + checker.diagnostics, + expr.span, + "C declaration '%s' is unavailable: %s", + symbol_text(checker, expr.name), + checker.ast_module.functions[template].unsupported_reason, + ) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue diff --git a/compiler/cimport/cimport.odin b/compiler/cimport/cimport.odin new file mode 100644 index 0000000..d86a7e0 --- /dev/null +++ b/compiler/cimport/cimport.odin @@ -0,0 +1,140 @@ +package cimport + +import "../target" +import "core:mem" + +Type_Id :: distinct u32 +INVALID_TYPE :: Type_Id(0xffff_ffff) + +Type_Kind :: enum u8 { + Invalid, + Void, + C_Char, + C_Schar, + C_Uchar, + C_Short, + C_Ushort, + C_Int, + C_Uint, + C_Long, + C_Ulong, + C_Longlong, + C_Ulonglong, + C_Float, + C_Double, + C_Longdouble, + Pointer, + Record, +} + +Type :: struct { + kind: Type_Kind, + child: Type_Id, + record: u32, + mutable: bool, +} + +Record :: struct { + name: string, + identity: string, +} + +Alias :: struct { + name: string, + type: Type_Id, + reason: string, +} + +Function :: struct { + name: string, + params: []Type_Id, + result: Type_Id, + reason: string, +} + +Unsupported :: struct { + name: string, + reason: string, +} + +Result :: struct { + types: [dynamic]Type, + records: [dynamic]Record, + aliases: [dynamic]Alias, + functions: [dynamic]Function, + unsupported: [dynamic]Unsupported, + error_message: string, + infrastructure: bool, + available: bool, + allocator: mem.Allocator, +} + +init_result :: proc(allocator := context.allocator) -> Result { + result: Result + result.allocator = allocator + result.types.allocator = allocator + result.records.allocator = allocator + result.aliases.allocator = allocator + result.functions.allocator = allocator + result.unsupported.allocator = allocator + return result +} + +destroy_result :: proc(result: ^Result) { + for record in result.records { + delete(record.name, result.allocator) + delete(record.identity, result.allocator) + } + for alias in result.aliases { + delete(alias.name, result.allocator) + delete(alias.reason, result.allocator) + } + for function in result.functions { + delete(function.name, result.allocator) + delete(function.params, result.allocator) + delete(function.reason, result.allocator) + } + for item in result.unsupported { + delete(item.name, result.allocator) + delete(item.reason, result.allocator) + } + delete(result.error_message, result.allocator) + delete(result.types) + delete(result.records) + delete(result.aliases) + delete(result.functions) + delete(result.unsupported) +} + +Request :: struct { + path: string, + include_paths: []string, + defines: []string, + target: target.Target, +} + +Backend_Proc :: proc(user_data: rawptr, request: Request, allocator: mem.Allocator) -> Result + +Backend :: struct { + import_header: Backend_Proc, + user_data: rawptr, +} + +Options :: struct { + include_paths: []string, + defines: []string, + backend: Backend, +} + +import_header :: proc(options: Options, path: string, selected := target.DEFAULT, allocator := context.allocator) -> Result { + request := Request{ + path=path, + include_paths=options.include_paths, + defines=options.defines, + target=selected, + } + if options.backend.import_header != nil { + return options.backend.import_header(options.backend.user_data, request, allocator) + } + return import_with_libclang(nil, request, allocator) +} diff --git a/compiler/cimport/libclang.odin b/compiler/cimport/libclang.odin new file mode 100644 index 0000000..38e7e7a --- /dev/null +++ b/compiler/cimport/libclang.odin @@ -0,0 +1,464 @@ +package cimport + +import "../target" +import "base:runtime" +import "core:dynlib" +import "core:fmt" +import "core:mem" +import "core:os/os2" +import "core:strings" + +CXCursor :: struct { + kind: i32, + xdata: i32, + data: [3]rawptr, +} + +CXType :: struct { + kind: i32, + data: [2]rawptr, +} + +CXString :: struct { + data: rawptr, + private_flags: u32, +} + +CXIndex :: distinct rawptr +CXTranslationUnit :: distinct rawptr +CXDiagnostic :: distinct rawptr + +Cursor_Visitor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 + +Api :: struct { + library: dynlib.Library, + + create_index: proc "c"(i32, i32) -> CXIndex, + dispose_index: proc "c"(CXIndex), + parse_translation_unit: proc "c"(CXIndex, cstring, [^]cstring, i32, rawptr, u32, u32, ^CXTranslationUnit) -> i32, + dispose_translation_unit: proc "c"(CXTranslationUnit), + get_translation_unit_cursor: proc "c"(CXTranslationUnit) -> CXCursor, + visit_children: proc "c"(CXCursor, Cursor_Visitor, rawptr) -> u32, + get_cursor_kind: proc "c"(CXCursor) -> i32, + get_cursor_spelling: proc "c"(CXCursor) -> CXString, + get_cursor_usr: proc "c"(CXCursor) -> CXString, + get_cursor_linkage: proc "c"(CXCursor) -> i32, + get_cursor_type: proc "c"(CXCursor) -> CXType, + get_typedef_underlying_type: proc "c"(CXCursor) -> CXType, + get_type_declaration: proc "c"(CXType) -> CXCursor, + get_canonical_type: proc "c"(CXType) -> CXType, + get_pointee_type: proc "c"(CXType) -> CXType, + get_result_type: proc "c"(CXType) -> CXType, + get_num_arg_types: proc "c"(CXType) -> i32, + get_arg_type: proc "c"(CXType, u32) -> CXType, + is_const_qualified_type: proc "c"(CXType) -> u32, + is_volatile_qualified_type: proc "c"(CXType) -> u32, + cursor_is_variadic: proc "c"(CXCursor) -> u32, + get_num_diagnostics: proc "c"(CXTranslationUnit) -> u32, + get_diagnostic: proc "c"(CXTranslationUnit, u32) -> CXDiagnostic, + get_diagnostic_severity: proc "c"(CXDiagnostic) -> i32, + get_diagnostic_spelling: proc "c"(CXDiagnostic) -> CXString, + dispose_diagnostic: proc "c"(CXDiagnostic), + get_cstring: proc "c"(CXString) -> cstring, + dispose_string: proc "c"(CXString), +} + +CXCursor_StructDecl :: i32(2) +CXCursor_UnionDecl :: i32(3) +CXCursor_EnumDecl :: i32(5) +CXCursor_FunctionDecl :: i32(8) +CXCursor_VarDecl :: i32(9) +CXCursor_TypedefDecl :: i32(20) +CXCursor_MacroDefinition :: i32(501) + +CXLinkage_External :: i32(4) + +CXType_Invalid :: i32(0) +CXType_Unexposed :: i32(1) +CXType_Void :: i32(2) +CXType_Char_U :: i32(4) +CXType_UChar :: i32(5) +CXType_UShort :: i32(8) +CXType_UInt :: i32(9) +CXType_ULong :: i32(10) +CXType_ULongLong :: i32(11) +CXType_Char_S :: i32(13) +CXType_SChar :: i32(14) +CXType_Short :: i32(16) +CXType_Int :: i32(17) +CXType_Long :: i32(18) +CXType_LongLong :: i32(19) +CXType_Float :: i32(21) +CXType_Double :: i32(22) +CXType_LongDouble :: i32(23) +CXType_Pointer :: i32(101) +CXType_Record :: i32(105) +CXType_Enum :: i32(106) +CXType_Typedef :: i32(107) +CXType_FunctionNoProto :: i32(110) +CXType_FunctionProto :: i32(111) +CXType_Elaborated :: i32(119) +CXType_Attributed :: i32(163) + +CXChildVisit_Continue :: i32(1) + +CXTranslationUnit_DetailedPreprocessingRecord :: u32(0x01) +CXTranslationUnit_SkipFunctionBodies :: u32(0x40) +CXTranslationUnit_KeepGoing :: u32(0x200) + +CXDiagnostic_Error :: i32(3) + +load_proc :: proc(api: ^Api, name: string, destination: ^$T) -> bool { + address, found := dynlib.symbol_address(api.library, name) + if !found { + return false + } + destination^ = transmute(T)address + return true +} + +load_api_from :: proc(path: string) -> (Api, bool) { + api: Api + library, loaded := dynlib.load_library(path) + if !loaded { + return {}, false + } + api.library = library + ok := + load_proc(&api, "clang_createIndex", &api.create_index) && + load_proc(&api, "clang_disposeIndex", &api.dispose_index) && + load_proc(&api, "clang_parseTranslationUnit2", &api.parse_translation_unit) && + load_proc(&api, "clang_disposeTranslationUnit", &api.dispose_translation_unit) && + load_proc(&api, "clang_getTranslationUnitCursor", &api.get_translation_unit_cursor) && + load_proc(&api, "clang_visitChildren", &api.visit_children) && + load_proc(&api, "clang_getCursorKind", &api.get_cursor_kind) && + load_proc(&api, "clang_getCursorSpelling", &api.get_cursor_spelling) && + load_proc(&api, "clang_getCursorUSR", &api.get_cursor_usr) && + load_proc(&api, "clang_getCursorLinkage", &api.get_cursor_linkage) && + load_proc(&api, "clang_getCursorType", &api.get_cursor_type) && + load_proc(&api, "clang_getTypedefDeclUnderlyingType", &api.get_typedef_underlying_type) && + load_proc(&api, "clang_getTypeDeclaration", &api.get_type_declaration) && + load_proc(&api, "clang_getCanonicalType", &api.get_canonical_type) && + load_proc(&api, "clang_getPointeeType", &api.get_pointee_type) && + load_proc(&api, "clang_getResultType", &api.get_result_type) && + load_proc(&api, "clang_getNumArgTypes", &api.get_num_arg_types) && + load_proc(&api, "clang_getArgType", &api.get_arg_type) && + load_proc(&api, "clang_isConstQualifiedType", &api.is_const_qualified_type) && + load_proc(&api, "clang_isVolatileQualifiedType", &api.is_volatile_qualified_type) && + load_proc(&api, "clang_Cursor_isVariadic", &api.cursor_is_variadic) && + load_proc(&api, "clang_getNumDiagnostics", &api.get_num_diagnostics) && + load_proc(&api, "clang_getDiagnostic", &api.get_diagnostic) && + load_proc(&api, "clang_getDiagnosticSeverity", &api.get_diagnostic_severity) && + load_proc(&api, "clang_getDiagnosticSpelling", &api.get_diagnostic_spelling) && + load_proc(&api, "clang_disposeDiagnostic", &api.dispose_diagnostic) && + load_proc(&api, "clang_getCString", &api.get_cstring) && + load_proc(&api, "clang_disposeString", &api.dispose_string) + if !ok { + _ = dynlib.unload_library(api.library) + return {}, false + } + return api, true +} + +load_api :: proc() -> (Api, bool) { + if override, found := os2.lookup_env_alloc("BROLANG_LIBCLANG_PATH", context.temp_allocator); found { + if api, ok := load_api_from(override); ok { + return api, true + } + } + candidates := [?]string{ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libclang.dylib", + "/Library/Developer/CommandLineTools/usr/lib/libclang.dylib", + "/opt/homebrew/opt/llvm/lib/libclang.dylib", + "/opt/homebrew/opt/llvm@21/lib/libclang.dylib", + "libclang.dylib", + "libclang.so", + } + for candidate in candidates { + if api, ok := load_api_from(candidate); ok { + return api, true + } + } + return {}, false +} + +clone_cx_string :: proc(api: ^Api, value: CXString, allocator: mem.Allocator) -> string { + defer api.dispose_string(value) + text := api.get_cstring(value) + if text == nil { + return fmt.aprintf("", allocator=allocator) + } + return fmt.aprintf("%s", string(text), allocator=allocator) +} + +Context :: struct { + api: ^Api, + result: ^Result, + allocator: mem.Allocator, +} + +add_type :: proc(ctx: ^Context, value: Type) -> Type_Id { + id := Type_Id(len(ctx.result.types)) + append(&ctx.result.types, value) + return id +} + +find_record :: proc(ctx: ^Context, identity: string) -> (u32, bool) { + for record, index in ctx.result.records { + if record.identity == identity { + return u32(index), true + } + } + return 0, false +} + +add_record :: proc(ctx: ^Context, declaration: CXCursor, preferred_name: string) -> u32 { + identity := clone_cx_string(ctx.api, ctx.api.get_cursor_usr(declaration), ctx.allocator) + if len(identity) == 0 { + delete(identity, ctx.allocator) + identity = clone_cx_string(ctx.api, ctx.api.get_cursor_spelling(declaration), ctx.allocator) + } + if index, ok := find_record(ctx, identity); ok { + delete(identity, ctx.allocator) + return index + } + name := fmt.aprintf("%s", preferred_name, allocator=ctx.allocator) + if len(name) == 0 { + delete(name, ctx.allocator) + name = clone_cx_string(ctx.api, ctx.api.get_cursor_spelling(declaration), ctx.allocator) + } + index := u32(len(ctx.result.records)) + append(&ctx.result.records, Record{name=name, identity=identity}) + return index +} + +translate_type :: proc(ctx: ^Context, value: CXType, preferred_record_name := "", depth := 0) -> Type_Id { + if depth > 64 || value.kind == CXType_Invalid || ctx.api.is_volatile_qualified_type(value) != 0 { + return INVALID_TYPE + } + switch value.kind { + case CXType_Void: return add_type(ctx, Type{kind=.Void, child=INVALID_TYPE}) + case CXType_Char_U: return add_type(ctx, Type{kind=.C_Char, child=INVALID_TYPE}) + case CXType_Char_S: return add_type(ctx, Type{kind=.C_Char, child=INVALID_TYPE}) + case CXType_SChar: return add_type(ctx, Type{kind=.C_Schar, child=INVALID_TYPE}) + case CXType_UChar: return add_type(ctx, Type{kind=.C_Uchar, child=INVALID_TYPE}) + case CXType_Short: return add_type(ctx, Type{kind=.C_Short, child=INVALID_TYPE}) + case CXType_UShort: return add_type(ctx, Type{kind=.C_Ushort, child=INVALID_TYPE}) + case CXType_Int: return add_type(ctx, Type{kind=.C_Int, child=INVALID_TYPE}) + case CXType_UInt: return add_type(ctx, Type{kind=.C_Uint, child=INVALID_TYPE}) + case CXType_Long: return add_type(ctx, Type{kind=.C_Long, child=INVALID_TYPE}) + case CXType_ULong: return add_type(ctx, Type{kind=.C_Ulong, child=INVALID_TYPE}) + case CXType_LongLong: return add_type(ctx, Type{kind=.C_Longlong, child=INVALID_TYPE}) + case CXType_ULongLong: return add_type(ctx, Type{kind=.C_Ulonglong, child=INVALID_TYPE}) + case CXType_Float: return add_type(ctx, Type{kind=.C_Float, child=INVALID_TYPE}) + case CXType_Double: return add_type(ctx, Type{kind=.C_Double, child=INVALID_TYPE}) + case CXType_LongDouble: return add_type(ctx, Type{kind=.C_Longdouble, child=INVALID_TYPE}) + case CXType_Pointer: + pointee := ctx.api.get_pointee_type(value) + child := translate_type(ctx, pointee, "", depth+1) + if child == INVALID_TYPE { + return INVALID_TYPE + } + return add_type(ctx, Type{ + kind=.Pointer, + child=child, + mutable=ctx.api.is_const_qualified_type(pointee) == 0, + }) + case CXType_Record: + declaration := ctx.api.get_type_declaration(value) + if ctx.api.get_cursor_kind(declaration) == CXCursor_UnionDecl { + return INVALID_TYPE + } + record := add_record(ctx, declaration, preferred_record_name) + return add_type(ctx, Type{kind=.Record, child=INVALID_TYPE, record=record}) + case CXType_Typedef: + declaration := ctx.api.get_type_declaration(value) + name := clone_cx_string(ctx.api, ctx.api.get_cursor_spelling(declaration), ctx.allocator) + defer delete(name, ctx.allocator) + return translate_type(ctx, ctx.api.get_typedef_underlying_type(declaration), name, depth+1) + case CXType_Elaborated, CXType_Attributed, CXType_Unexposed: + canonical := ctx.api.get_canonical_type(value) + if canonical.kind == value.kind { + return INVALID_TYPE + } + return translate_type(ctx, canonical, preferred_record_name, depth+1) + case CXType_Enum, CXType_FunctionNoProto, CXType_FunctionProto: + return INVALID_TYPE + } + return INVALID_TYPE +} + +has_named :: proc(items: []Unsupported, name: string) -> bool { + for item in items { + if item.name == name { + return true + } + } + return false +} + +add_unsupported :: proc(ctx: ^Context, name, reason: string) { + if len(name) == 0 || strings.has_prefix(name, "__") || has_named(ctx.result.unsupported[:], name) { + return + } + append(&ctx.result.unsupported, Unsupported{ + name=fmt.aprintf("%s", name, allocator=ctx.allocator), + reason=fmt.aprintf("%s", reason, allocator=ctx.allocator), + }) +} + +add_alias :: proc(ctx: ^Context, name: string, value: Type_Id, reason := "") { + if len(name) == 0 { + return + } + for alias in ctx.result.aliases { + if alias.name == name { + return + } + } + append(&ctx.result.aliases, Alias{ + name=fmt.aprintf("%s", name, allocator=ctx.allocator), + type=value, + reason=fmt.aprintf("%s", reason, allocator=ctx.allocator), + }) +} + +visit_cursor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { + context = runtime.default_context() + ctx := (^Context)(client_data) + kind := ctx.api.get_cursor_kind(cursor) + name := clone_cx_string(ctx.api, ctx.api.get_cursor_spelling(cursor), ctx.allocator) + defer delete(name, ctx.allocator) + switch kind { + case CXCursor_FunctionDecl: + if len(name) == 0 { + return CXChildVisit_Continue + } + reason := "" + linkage := ctx.api.get_cursor_linkage(cursor) + if linkage != CXLinkage_External { + reason = "static and non-external C functions are not supported" + } else if ctx.api.cursor_is_variadic(cursor) != 0 { + reason = "C variadic functions are not supported" + } + function_type := ctx.api.get_cursor_type(cursor) + result_type := translate_type(ctx, ctx.api.get_result_type(function_type)) + if result_type == INVALID_TYPE && len(reason) == 0 { + reason = "function result type is not supported" + } + params: [dynamic]Type_Id + params.allocator = ctx.allocator + count := ctx.api.get_num_arg_types(function_type) + if count < 0 { + reason = "function declaration has no prototype" + } else { + for index in 0.. 0 { + record := add_record(ctx, cursor, name) + value := add_type(ctx, Type{kind=.Record, child=INVALID_TYPE, record=record}) + add_alias(ctx, name, value) + } + case CXCursor_UnionDecl: + add_unsupported(ctx, name, "C unions are not supported") + case CXCursor_EnumDecl: + add_unsupported(ctx, name, "C enums are not supported") + case CXCursor_VarDecl: + add_unsupported(ctx, name, "external C variables are not supported") + case CXCursor_MacroDefinition: + add_unsupported(ctx, name, "C macros are not supported") + case: + } + return CXChildVisit_Continue +} + +import_with_libclang :: proc(_: rawptr, request: Request, allocator: mem.Allocator) -> Result { + result := init_result(allocator) + api, loaded := load_api() + if !loaded { + result.infrastructure = true + result.error_message = fmt.aprintf( + "could not load libclang; set BROLANG_LIBCLANG_PATH to a compatible library", + allocator=allocator, + ) + return result + } + defer _ = dynlib.unload_library(api.library) + + index := api.create_index(1, 0) + if index == nil { + result.infrastructure = true + result.error_message = fmt.aprintf("could not create libclang index", allocator=allocator) + return result + } + defer api.dispose_index(index) + + arguments: [dynamic]string + arguments.allocator = context.temp_allocator + append(&arguments, "-x", "c", "-target", target.llvm_triple(request.target)) + for path in request.include_paths { + append(&arguments, fmt.tprintf("-I%s", path)) + } + for define in request.defines { + append(&arguments, fmt.tprintf("-D%s", define)) + } + c_arguments := make([]cstring, len(arguments), context.temp_allocator) + for argument, argument_index in arguments { + c_arguments[argument_index] = strings.clone_to_cstring(argument, context.temp_allocator) + } + c_path := strings.clone_to_cstring(request.path, context.temp_allocator) + translation_unit: CXTranslationUnit + error_code := api.parse_translation_unit( + index, + c_path, + raw_data(c_arguments), + i32(len(c_arguments)), + nil, + 0, + CXTranslationUnit_DetailedPreprocessingRecord | CXTranslationUnit_SkipFunctionBodies | CXTranslationUnit_KeepGoing, + &translation_unit, + ) + if error_code != 0 || translation_unit == nil { + result.error_message = fmt.aprintf("libclang could not parse header '%s'", request.path, allocator=allocator) + return result + } + defer api.dispose_translation_unit(translation_unit) + + for diagnostic_index in 0..= CXDiagnostic_Error && len(result.error_message) == 0 { + result.error_message = clone_cx_string(&api, api.get_diagnostic_spelling(diagnostic), allocator) + } + api.dispose_diagnostic(diagnostic) + } + if len(result.error_message) > 0 { + return result + } + + ctx := Context{api=&api, result=&result, allocator=allocator} + root := api.get_translation_unit_cursor(translation_unit) + _ = api.visit_children(root, visit_cursor, &ctx) + result.available = true + return result +} diff --git a/compiler/compiler.odin b/compiler/compiler.odin index 7b787c8..a12a76c 100644 --- a/compiler/compiler.odin +++ b/compiler/compiler.odin @@ -1,6 +1,7 @@ package compiler import "./backend" +import "./cimport" import "./checker" import "./llvm" import "./linker" @@ -19,6 +20,7 @@ compile_package :: proc( input_path, output_path: string, link_arguments: []linker.Argument = nil, selected := target.DEFAULT, + c_options := cimport.Options{}, ) -> int { sources := source.init_store() defer source.destroy_store(&sources) @@ -59,6 +61,8 @@ compile_package :: proc( &symbols, vmem.arena_allocator(&lexer_arena), vmem.arena_allocator(&parser_arena), + c_options, + selected, ) if !loaded { source.print_all(&diagnostics) @@ -83,7 +87,7 @@ compile_package :: proc( } source.print_all(&diagnostics) - if !backend.compile(llvm_path, output_path, link_arguments, selected) { + if !backend.compile(llvm_path, output_path, link_arguments, selected, c_options) { return 2 } if len(diagnostics.items) > 0 { diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 7df3941..f10a134 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -93,6 +93,7 @@ Expr_Kind :: enum u8 { Unwrap, Orelse, Widen, + Weaken_Pointer, Negate, Add, Pointer_Add, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index 1e9f320..eb2d826 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -87,6 +87,7 @@ Opcode :: enum u8 { Orelse_Begin, Orelse, Widen, + Weaken_Pointer, Neg_Checked, Add_Checked, Pointer_Add, diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 18af1f7..9be0972 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -112,7 +112,7 @@ valid_value :: proc( switch instructions[value_id].op { case .Param, .Const, .String, .Aggregate, .None, .Optional_Some, .Load_Global, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr, .Unwrap, .Orelse, - .Widen, .Neg_Checked, .Add_Checked, .Pointer_Add, .Call: + .Widen, .Weaken_Pointer, .Neg_Checked, .Add_Checked, .Pointer_Add, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, .Store, .Trap, .Return, .Return_Void: @@ -707,6 +707,13 @@ emit_instruction_stream :: proc( fmt.sbprintf(&emitter.builder, " %%v%d = %s %s ", instruction_index, operation, llvm_type(from_type, &emitter.module.types)) write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type, &emitter.module.types)) + case .Weaken_Pointer: + if !valid_instruction(instructions, instruction.a) || + !types.can_weaken_pointer(instructions[instruction.a].type, instruction.type, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid pointer weakening operand") + continue + } + fmt.sbprintf(&emitter.builder, " %%v%d = select i1 true, ptr %%v%d, ptr null\n", instruction_index, instruction.a) case .Neg_Checked: if !valid_value(instructions, instruction.a, instruction.type, &emitter.module.types) { emit_recovery_value(emitter, instruction_index, instruction, "invalid negation operand") @@ -985,7 +992,19 @@ emit_constructor :: proc(emitter: ^Emitter) { } emit_functions :: proc(emitter: ^Emitter) { - for function in emitter.module.functions { + for function, function_index in emitter.module.functions { + if function.implementation == .Declaration { + duplicate := false + for previous in emitter.module.functions[:function_index] { + if previous.link_name == function.link_name { + duplicate = true + break + } + } + if duplicate { + continue + } + } if function.implementation == .Declaration { strings.write_string(&emitter.builder, "declare ") } else { diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 2ce0242..20cf65f 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -1,11 +1,14 @@ package loader import "../ast" +import "../cimport" import "../lexer" import "../parser" import "../source" import "../symbol" +import "../target" import "../types" +import "core:fmt" import "core:mem" import "core:os" import "core:path/filepath" @@ -19,6 +22,10 @@ State :: struct { symbols: ^symbol.Table, token_allocator: mem.Allocator, allocator: mem.Allocator, + c_options: cimport.Options, + selected: target.Target, + record_identities: [dynamic]string, + record_types: [dynamic]types.Type, root_failed: bool, } @@ -104,6 +111,214 @@ resolve_import_path :: proc(state: ^State, importing_path, import_path: string) return joined, false } +header_package_name :: proc(path: string, symbols: ^symbol.Table) -> symbol.Id { + base := filepath.base(path) + extension := filepath.ext(base) + if len(extension) > 0 { + base = base[:len(base)-len(extension)] + } + return symbol.intern(symbols, base) +} + +find_record_identity :: proc(state: ^State, identity: string) -> types.Type { + for existing, index in state.record_identities { + if existing == identity { + return state.record_types[index] + } + } + return types.INVALID +} + +translate_c_type :: proc( + state: ^State, + result: ^cimport.Result, + value: cimport.Type_Id, + pkg: ast.Package_Id, + record_mapping: []types.Type, + type_mapping: []types.Type, +) -> types.Type { + if value == cimport.INVALID_TYPE || int(value) < 0 || int(value) >= len(result.types) { + return types.INVALID + } + if types.is_valid(type_mapping[value]) { + return type_mapping[value] + } + item := result.types[value] + translated := types.INVALID + switch item.kind { + case .Invalid: translated = types.INVALID + case .Void: translated = types.VOID + case .C_Char: translated = types.C_CHAR + case .C_Schar: translated = types.C_SCHAR + case .C_Uchar: translated = types.C_UCHAR + case .C_Short: translated = types.C_SHORT + case .C_Ushort: translated = types.C_USHORT + case .C_Int: translated = types.C_INT + case .C_Uint: translated = types.C_UINT + case .C_Long: translated = types.C_LONG + case .C_Ulong: translated = types.C_ULONG + case .C_Longlong: translated = types.C_LONGLONG + case .C_Ulonglong: translated = types.C_ULONGLONG + case .C_Float: translated = types.C_FLOAT + case .C_Double: translated = types.C_DOUBLE + case .C_Longdouble: translated = types.C_LONGDOUBLE + case .Pointer: + child := translate_c_type(state, result, item.child, pkg, record_mapping, type_mapping) + if types.is_valid(child) { + pointer := types.pointer(&state.module.type_store, child, item.mutable, true) + translated = types.optional(&state.module.type_store, pointer) + } + case .Record: + if int(item.record) < len(record_mapping) { + translated = record_mapping[item.record] + } + } + type_mapping[value] = translated + return translated +} + +function_signatures_equal :: proc(left: ast.Function, params: []ast.Param, result: types.Type) -> bool { + if left.result != result || len(left.params) != len(params) { + return false + } + for param, index in params { + if left.params[index].type != param.type { + return false + } + } + return true +} + +load_header :: proc(state: ^State, path: string, import_span: source.Span) -> ast.Package_Id { + canonical, ok := filepath.abs(path, state.allocator) + if !ok { + id := add_placeholder(state, path) + source.addf(state.diagnostics, import_span, "could not resolve C header '%s'", path) + return id + } + if existing := find_package(state, canonical); existing != ast.INVALID_PACKAGE { + delete(canonical, state.allocator) + return existing + } + pkg_id := ast.package_id(len(state.module.packages)) + append(&state.module.packages, ast.Package{ + path=canonical, + name=header_package_name(canonical, state.symbols), + available=false, + kind=.C_Header, + }) + result := cimport.import_header(state.c_options, canonical, state.selected, state.allocator) + defer cimport.destroy_result(&result) + if !result.available { + message := result.error_message if len(result.error_message) > 0 else "C header import failed" + source.addf(state.diagnostics, import_span, "could not import C header '%s': %s", path, message) + if result.infrastructure { + state.root_failed = true + } + return pkg_id + } + state.module.packages[pkg_id].available = true + + record_mapping := make([]types.Type, len(result.records), state.allocator) + defer delete(record_mapping, state.allocator) + for record, index in result.records { + record_type := find_record_identity(state, record.identity) + if !types.is_valid(record_type) { + name := record.name + if len(name) == 0 { + name = fmt.tprintf("__c_record_%d", len(state.record_types)) + } + record_type = types.named(&state.module.type_store, u32(pkg_id), u32(symbol.intern(state.symbols, name))) + _ = types.define_struct(&state.module.type_store, record_type, nil, true, true) + append(&state.record_identities, strings.clone(record.identity, state.allocator)) + append(&state.record_types, record_type) + } + record_mapping[index] = record_type + } + type_mapping := make([]types.Type, len(result.types), state.allocator) + defer delete(type_mapping, state.allocator) + + for alias in result.aliases { + name := symbol.intern(state.symbols, alias.name) + id := types.named(&state.module.type_store, u32(pkg_id), u32(name)) + child := translate_c_type(state, &result, alias.type, pkg_id, record_mapping, type_mapping) + _ = types.define_alias(&state.module.type_store, id, child) + if len(alias.reason) > 0 { + append(&state.module.unsupported, ast.Unsupported{ + pkg=pkg_id, + name=name, + reason=strings.clone(alias.reason, state.allocator), + }) + } + } + + for function in result.functions { + params := make([]ast.Param, len(function.params), state.allocator) + for param_type, index in function.params { + params[index] = ast.Param{ + name=symbol.intern(state.symbols, fmt.tprintf("arg%d", index)), + span=import_span, + type=translate_c_type(state, &result, param_type, pkg_id, record_mapping, type_mapping), + } + } + function_result := translate_c_type(state, &result, function.result, pkg_id, record_mapping, type_mapping) + unsupported_reason := function.reason + if len(unsupported_reason) == 0 { + for param in params { + if types.contains_c_struct_by_value(param.type, &state.module.type_store) { + unsupported_reason = "C records passed by value are not supported" + break + } + } + } + if len(unsupported_reason) == 0 && + types.contains_c_struct_by_value(function_result, &state.module.type_store) { + unsupported_reason = "C records returned by value are not supported" + } + name := symbol.intern(state.symbols, function.name) + duplicate := false + for &existing in state.module.functions { + if existing.pkg != pkg_id || existing.name != name { + continue + } + duplicate = true + if !function_signatures_equal(existing, params, function_result) && len(existing.unsupported_reason) == 0 { + existing.unsupported_reason = fmt.aprintf( + "conflicting C declarations for '%s'", + function.name, + allocator=state.allocator, + ) + } + break + } + if duplicate { + delete(params, state.allocator) + continue + } + append(&state.module.functions, ast.Function{ + span=import_span, + name=name, + pkg=pkg_id, + file=ast.INVALID_FILE, + c_abi=true, + imported=true, + has_body=false, + params=params, + result=function_result, + unsupported_reason=strings.clone(unsupported_reason, state.allocator), + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + for item in result.unsupported { + append(&state.module.unsupported, ast.Unsupported{ + pkg=pkg_id, + name=symbol.intern(state.symbols, item.name), + reason=strings.clone(item.reason, state.allocator), + }) + } + return pkg_id +} + load_package :: proc(state: ^State, path: string, import_span: source.Span, is_root := false) -> ast.Package_Id { canonical, ok := filepath.abs(path, state.allocator) if !ok || !os.is_dir(path) { @@ -191,7 +406,7 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r continue } target_path, target_ok := resolve_import_path(state, canonical, import_item.path) - target := load_package(state, target_path, import_item.span) + target := load_header(state, target_path, import_item.span) if filepath.ext(import_item.path) == ".h" else load_package(state, target_path, import_item.span) state.module.imports[import_id].target = target if !target_ok || target == ast.INVALID_PACKAGE || !state.module.packages[target].available { state.module.imports[import_id].valid = false @@ -293,14 +508,19 @@ canonical_type :: proc( import_item := module.imports[import_id] resolved := types.find_named(&module.type_store, u32(import_item.target), item.name) if types.is_valid(resolved) { - mapping[index] = resolved - return resolved + mapping[index] = canonical_type(module, resolved, mapping, visiting) + return mapping[index] } } } mapping[index] = value return value } + if item.kind == .Alias { + resolved := canonical_type(module, item.child, mapping, visiting) + mapping[index] = value if !types.is_valid(resolved) else resolved + return mapping[index] + } if item.kind == .Struct { mapping[index] = value fields := types.fields_for(&module.type_store, value) @@ -347,6 +567,8 @@ load :: proc( symbols: ^symbol.Table, token_allocator := context.allocator, allocator := context.allocator, + c_options := cimport.Options{}, + selected := target.DEFAULT, ) -> (ast.Module, bool) { module := ast.init_module(allocator) state := State{ @@ -356,6 +578,17 @@ load :: proc( symbols=symbols, token_allocator=token_allocator, allocator=allocator, + c_options=c_options, + selected=selected, + } + state.record_identities.allocator = allocator + state.record_types.allocator = allocator + defer { + for identity in state.record_identities { + delete(identity, allocator) + } + delete(state.record_identities) + delete(state.record_types) } root := load_package(&state, root_path, source.Span{}, true) if root != ast.Package_Id(0) && root != ast.INVALID_PACKAGE { diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 1750590..4d8d157 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -324,7 +324,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { }) } _ = pop(&stack) - case .Widen: + case .Widen, .Weaken_Pointer: stack[frame_index].stage = 1 append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Negate: @@ -358,7 +358,8 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { } if frame.stage == 1 { last = append_instruction(state, ir.Instruction{ - op=.Widen, span=expr.span, type=expr.type, target=ir.INVALID_REF, + op=.Weaken_Pointer if expr.kind == .Weaken_Pointer else .Widen, + span=expr.span, type=expr.type, target=ir.INVALID_REF, a=last, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) _ = pop(&stack) diff --git a/compiler/types/types.odin b/compiler/types/types.odin index b14439e..ee7b500 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -59,6 +59,7 @@ Kind :: enum u8 { Slice, Optional, Named, + Alias, Struct, } @@ -132,7 +133,7 @@ intern :: proc(store: ^Store, candidate: Node) -> Type { named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0, file: u32 = 0xffff_ffff) -> Type { normalized_file := file if qualifier != 0 else u32(0) for existing, index in store.nodes { - if (existing.kind == .Named || existing.kind == .Struct) && + if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Struct) && existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier && existing.file == normalized_file { return DYNAMIC_START+Type(index) @@ -143,7 +144,7 @@ named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0, file: u32 = 0xf find_named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0) -> Type { for existing, index in store.nodes { - if (existing.kind == .Named || existing.kind == .Struct) && + if (existing.kind == .Named || existing.kind == .Alias || existing.kind == .Struct) && existing.pkg == pkg && existing.name == name && existing.qualifier == qualifier { return DYNAMIC_START+Type(index) } @@ -151,6 +152,18 @@ find_named :: proc(store: ^Store, pkg, name: u32, qualifier: u32 = 0) -> Type { return INVALID } +define_alias :: proc(store: ^Store, id, child: Type) -> bool { + existing, ok := node(store, id) + if !ok || existing.kind != .Named || existing.declared { + return false + } + index := int(id-DYNAMIC_START) + store.nodes[index].kind = .Alias + store.nodes[index].child = child + store.nodes[index].declared = true + return true +} + define_struct :: proc(store: ^Store, id: Type, fields: []Field, c_layout, opaque: bool) -> bool { existing, ok := node(store, id) if !ok || (existing.kind != .Named && existing.kind != .Struct) || existing.declared { diff --git a/compiler_tests.odin b/compiler_tests.odin index 5d80262..c4b30a2 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -3,6 +3,7 @@ package main import compiler_core "./compiler" import "./compiler/ast" import "./compiler/backend" +import "./compiler/cimport" import "./compiler/checker" import "./compiler/hir" import "./compiler/ir" @@ -18,6 +19,7 @@ import "./compiler/target" import "./compiler/token" import "./compiler/types" import "core:fmt" +import "core:mem" import "core:os" import "core:os/os2" import "core:strings" @@ -395,20 +397,26 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T) options, valid := parse_cli_args([]string{ "brolang", "app", - "--link", + "--c-link", "native.c", "-o", "app.out", - "--library-path", + "--c-library-path", "vendor/lib", - "--library", + "--c-library", "thing", - "--link", + "--c-link", "helper.o", + "--c-include-path", + "vendor/include", + "--c-define", + "FEATURE=1", "--target", "aarch64-macos", }) defer delete(options.link_arguments) + defer delete(options.c_options.include_paths) + defer delete(options.c_options.defines) testing.expect(t, valid) testing.expect_value(t, options.input_path, "app") @@ -419,6 +427,8 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T) 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") + testing.expect_value(t, options.c_options.include_paths[0], "vendor/include") + testing.expect_value(t, options.c_options.defines[0], "FEATURE=1") testing.expect_value(t, target.name(options.target), "aarch64-macos") _, unknown_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--unknown", "value"}) @@ -426,11 +436,17 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T) _, 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"}) _, invalid_target := parse_cli_args([]string{"brolang", "app", "-o", "out", "--target", "x86_64-linux"}) + _, legacy_link := parse_cli_args([]string{"brolang", "app", "-o", "out", "--link", "native.c"}) + _, legacy_library_path := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library-path", "vendor/lib"}) + _, legacy_library := parse_cli_args([]string{"brolang", "app", "-o", "out", "--library", "thing"}) testing.expect(t, !unknown_valid) testing.expect(t, !incomplete_valid) testing.expect(t, !duplicate_output_valid) testing.expect(t, !duplicate_empty_output_valid) testing.expect(t, !invalid_target) + testing.expect(t, !legacy_link) + testing.expect(t, !legacy_library_path) + testing.expect(t, !legacy_library) } @(test) @@ -1319,6 +1335,44 @@ foreign_function_links_from_c_source :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 42) } +@(test) +restricted_c_header_imports_compile_and_link :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-header-import" + defer _ = os.remove(output) + arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}} + c_options := cimport.Options{ + include_paths=[]string{"examples/interop/header/include"}, + defines=[]string{"BROLANG_FEATURE"}, + } + status := compiler_core.compile_package("examples/interop/header/app", output, arguments, target.DEFAULT, c_options) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +unsupported_c_header_members_diagnose_only_when_referenced :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-header-unsupported" + defer _ = os.remove(output) + c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}} + status := compiler_core.compile_package("examples/interop/header_unsupported", output, nil, target.DEFAULT, c_options) + testing.expect_value(t, status, 1) + state := run_executable(output) + testing.expect(t, !state.success) +} + +@(test) +compatible_c_header_redeclarations_share_one_llvm_declaration :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-header-duplicate" + defer _ = os.remove(output) + arguments := []linker.Argument{{kind=.Input, value="examples/interop/header/native.c"}} + c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}} + status := compiler_core.compile_package("examples/interop/header_duplicate", output, arguments, target.DEFAULT, c_options) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + @(test) interop_foundation_matches_zig_compiled_apple_silicon_c_fixture :: proc(t: ^testing.T) { output := "/tmp/brolang-test-interop-foundation" @@ -1570,7 +1624,11 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T) {kind=.Library, value="thing"}, {kind=.Input, value="helper.o"}, } - command := backend.build_command("module.ll", "program", arguments) + c_options := cimport.Options{ + include_paths=[]string{"vendor/include"}, + defines=[]string{"FEATURE=1"}, + } + command := backend.build_command("module.ll", "program", arguments, target.DEFAULT, c_options) defer backend.destroy_command(command) expected := []string{ @@ -1580,7 +1638,10 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T) "-target", "aarch64-macos", "-Wno-override-module", + "-Wno-unused-command-line-argument", "module.ll", + "-Ivendor/include", + "-DFEATURE=1", "native.c", "-Lvendor/lib", "-lthing", @@ -1594,6 +1655,109 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T) } } +Fake_Cimport_State :: struct { + calls: int, + available: bool, + infrastructure: bool, + saw_options: bool, +} + +fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result { + state := (^Fake_Cimport_State)(user_data) + state.calls += 1 + state.saw_options = + len(request.include_paths) == 2 && + request.include_paths[0] == "first/include" && + request.include_paths[1] == "second/include" && + len(request.defines) == 2 && + request.defines[0] == "FIRST=1" && + request.defines[1] == "SECOND" && + request.target == target.DEFAULT + result := cimport.init_result(allocator) + if !state.available { + result.infrastructure = state.infrastructure + result.error_message = fmt.aprintf("fake importer unavailable", allocator=allocator) + return result + } + append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE}) + append(&result.functions, cimport.Function{ + name=fmt.aprintf("fake_value", allocator=allocator), + result=cimport.Type_Id(0), + reason=fmt.aprintf("", allocator=allocator), + }) + result.available = true + return result +} + +@(test) +cimport_backend_is_replaceable :: proc(t: ^testing.T) { + state := Fake_Cimport_State{available=true} + options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}} + result := cimport.import_header(options, "fake.h") + defer cimport.destroy_result(&result) + + testing.expect(t, result.available) + testing.expect_value(t, state.calls, 1) + testing.expect_value(t, len(result.functions), 1) + testing.expect_value(t, result.functions[0].name, "fake_value") +} + +@(test) +loader_injects_and_caches_cimport_backend_per_compilation :: 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) + state := Fake_Cimport_State{available=true} + options := cimport.Options{ + include_paths=[]string{"first/include", "second/include"}, + defines=[]string{"FIRST=1", "SECOND"}, + backend={import_header=fake_cimport_backend, user_data=&state}, + } + module, loaded := loader.load( + "examples/interop/header_cache", + &sources, + &diagnostics, + &symbols, + c_options=options, + ) + defer ast.destroy_module(&module) + + testing.expect(t, loaded) + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, state.calls, 1) + testing.expect(t, state.saw_options) + testing.expect_value(t, len(module.imports), 2) + testing.expect_value(t, module.imports[0].target, module.imports[1].target) + testing.expect_value(t, len(module.functions), 2) +} + +@(test) +cimport_infrastructure_failure_makes_compilation_unavailable :: 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) + state := Fake_Cimport_State{infrastructure=true} + options := cimport.Options{backend={import_header=fake_cimport_backend, user_data=&state}} + module, loaded := loader.load( + "examples/interop/header_cache", + &sources, + &diagnostics, + &symbols, + c_options=options, + ) + defer ast.destroy_module(&module) + + testing.expect(t, !loaded) + testing.expect_value(t, state.calls, 1) + testing.expect(t, len(diagnostics.items) > 0) +} + @(test) source_store_owns_buffers_indexes_lines_and_deduplicates_diagnostics :: proc(t: ^testing.T) { store := source.init_store() diff --git a/examples/interop/header/app/main.bro b/examples/interop/header/app/main.bro new file mode 100644 index 0000000..fd7d119 --- /dev/null +++ b/examples/interop/header/app/main.bro @@ -0,0 +1,13 @@ +native :: import "../include/native.h" + +pass_alias :: func(value native.imported_int_alias) native.imported_int { + return value +} + +main :: func() void { + _ = native.imported_add(pass_alias(20), 22) + _ = native.imported_scalar(3, 4) + _ = native.child_value(7) + _ = native.imported_read(native.imported_handle()?) + _ = native.configured_value(9) +} diff --git a/examples/interop/header/include/child.h b/examples/interop/header/include/child.h new file mode 100644 index 0000000..a762f95 --- /dev/null +++ b/examples/interop/header/include/child.h @@ -0,0 +1,6 @@ +#ifndef BROLANG_CHILD_H +#define BROLANG_CHILD_H + +int child_value(int value); + +#endif diff --git a/examples/interop/header/include/native.h b/examples/interop/header/include/native.h new file mode 100644 index 0000000..36838d6 --- /dev/null +++ b/examples/interop/header/include/native.h @@ -0,0 +1,40 @@ +#ifndef BROLANG_NATIVE_H +#define BROLANG_NATIVE_H + +#include + +typedef int imported_int; +typedef imported_int imported_int_alias; +typedef struct Imported_Handle Imported_Handle; +typedef void (*Imported_Callback)(int value); +typedef struct Imported_Value { + int value; +} Imported_Value; +typedef union Imported_Union { + int value; +} Imported_Union; +typedef enum Imported_Enum { + IMPORTED_ENUM_VALUE, +} Imported_Enum; + +int imported_add(imported_int_alias left, int right); +unsigned long imported_scalar(unsigned char value, unsigned long extra); +Imported_Handle *imported_handle(void); +int imported_read(const Imported_Handle *handle); +Imported_Value imported_by_value(Imported_Value value); +int imported_volatile(volatile int *value); +_Bool imported_bool(_Bool value); +extern int imported_global; + +static inline int imported_inline(int value) { + return value; +} + +#ifdef BROLANG_FEATURE +int configured_value(int value); +#endif + +#define IMPORTED_MACRO 42 +int imported_variadic(const char *format, ...); + +#endif diff --git a/examples/interop/header/native.c b/examples/interop/header/native.c new file mode 100644 index 0000000..bce8d6f --- /dev/null +++ b/examples/interop/header/native.c @@ -0,0 +1,33 @@ +#include "include/native.h" + +struct Imported_Handle { + int value; +}; + +static struct Imported_Handle handle = {7}; + +int child_value(int value) { + return value; +} + +int imported_add(imported_int left, int right) { + return left + right; +} + +unsigned long imported_scalar(unsigned char value, unsigned long extra) { + return value + extra; +} + +Imported_Handle *imported_handle(void) { + return &handle; +} + +int imported_read(const Imported_Handle *value) { + return value->value; +} + +#ifdef BROLANG_FEATURE +int configured_value(int value) { + return value; +} +#endif diff --git a/examples/interop/header_cache/main.bro b/examples/interop/header_cache/main.bro new file mode 100644 index 0000000..323cd8d --- /dev/null +++ b/examples/interop/header_cache/main.bro @@ -0,0 +1,4 @@ +first :: import "../header/include/native.h" +second :: import "../header/include/native.h" + +main :: func() void {} diff --git a/examples/interop/header_duplicate/main.bro b/examples/interop/header_duplicate/main.bro new file mode 100644 index 0000000..1913a8a --- /dev/null +++ b/examples/interop/header_duplicate/main.bro @@ -0,0 +1,7 @@ +native :: import "../header/include/native.h" +child :: import "../header/include/child.h" + +main :: func() void { + _ = native.child_value(1) + _ = child.child_value(2) +} diff --git a/examples/interop/header_unsupported/main.bro b/examples/interop/header_unsupported/main.bro new file mode 100644 index 0000000..3999bd6 --- /dev/null +++ b/examples/interop/header_unsupported/main.bro @@ -0,0 +1,15 @@ +native :: import "../header/include/native.h" + +use_callback :: c_func(value native.Imported_Callback) void +use_union :: c_func(value native.Imported_Union) void +use_enum :: c_func(value native.Imported_Enum) void + +main :: func() void { + _ = native.imported_variadic("value") + _ = native.IMPORTED_MACRO + _ = native.imported_by_value() + _ = native.imported_volatile() + _ = native.imported_bool() + _ = native.imported_global + _ = native.imported_inline(1) +} diff --git a/main.odin b/main.odin index 8489dbd..f9c6aaf 100644 --- a/main.odin +++ b/main.odin @@ -1,6 +1,7 @@ package main import "./compiler" +import "./compiler/cimport" import "./compiler/linker" import "./compiler/target" import "core:fmt" @@ -10,6 +11,7 @@ Cli_Options :: struct { input_path: string, output_path: string, link_arguments: []linker.Argument, + c_options: cimport.Options, target: target.Target, } @@ -20,13 +22,24 @@ parse_cli_args :: proc(args: []string, allocator := context.allocator) -> (Cli_O options := Cli_Options{input_path=args[1], target=target.DEFAULT} link_arguments: [dynamic]linker.Argument link_arguments.allocator = allocator + include_paths: [dynamic]string + include_paths.allocator = allocator + defines: [dynamic]string + defines.allocator = allocator + success := false + defer { + if !success { + delete(link_arguments) + delete(include_paths) + delete(defines) + } + } 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] @@ -34,40 +47,43 @@ parse_cli_args :: proc(args: []string, allocator := context.allocator) -> (Cli_O switch option { case "-o": if output_set { - delete(link_arguments) return {}, false } output_set = true options.output_path = value - case "--link": + case "--c-link": append(&link_arguments, linker.Argument{kind=.Input, value=value}) - case "--library-path": + case "--c-library-path": append(&link_arguments, linker.Argument{kind=.Library_Path, value=value}) - case "--library": + case "--c-library": append(&link_arguments, linker.Argument{kind=.Library, value=value}) + case "--c-include-path": + append(&include_paths, value) + case "--c-define": + append(&defines, value) case "--target": selected, ok := target.parse(value) if !ok { - delete(link_arguments) return {}, false } options.target = selected case: - delete(link_arguments) return {}, false } } if !output_set || len(options.output_path) == 0 { - delete(link_arguments) return {}, false } options.link_arguments = link_arguments[:] + options.c_options.include_paths = include_paths[:] + options.c_options.defines = defines[:] + success = true return options, true } print_usage :: proc() { fmt.eprintln( - "usage: brolang -o [--target aarch64-macos] [--link | --library-path | --library ]...", + "usage: brolang -o [--target aarch64-macos] [--c-link | --c-library-path | --c-library | --c-include-path | --c-define ]...", ) } @@ -78,7 +94,9 @@ main :: proc() { os2.exit(2) } defer delete(options.link_arguments) - status := compiler.compile_package(options.input_path, options.output_path, options.link_arguments, options.target) + defer delete(options.c_options.include_paths) + defer delete(options.c_options.defines) + status := compiler.compile_package(options.input_path, options.output_path, options.link_arguments, options.target, options.c_options) if status != 0 { os2.exit(status) }