From 19e9fbdd4bed806e2544fcb5b8c9864665886fcb Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Wed, 17 Jun 2026 21:52:05 +0200 Subject: [PATCH] extern variables and object-like macro consts --- TODO.md | 211 ++-- compiler/ast/ast.odin | 26 + compiler/checker/checker.odin | 174 +++- compiler/cimport/cimport.odin | 82 +- compiler/cimport/libclang.odin | 932 +++++++++++++++++- compiler/compiler.odin | 62 +- compiler/hir/hir.odin | 4 + compiler/ir/ir.odin | 4 + compiler/llvm/llvm.odin | 48 +- compiler/loader/loader.odin | 646 +++++++++++- compiler/lower/lower.odin | 5 +- compiler_tests.odin | 724 ++++++++++++++ examples/interop/header/app/main.bro | 34 + examples/interop/header/include/child.h | 1 + examples/interop/header/include/guarded.h | 6 + examples/interop/header/include/native.h | 134 ++- examples/interop/header/include/once.h | 3 + examples/interop/header/include/repeat.h | 1 + examples/interop/header/native.c | 100 ++ examples/interop/header_conflict/first.h | 1 + examples/interop/header_conflict/main.bro | 7 + examples/interop/header_conflict/second.h | 1 + examples/interop/header_duplicate/main.bro | 3 + .../interop/header_main_conflict/main.bro | 5 + .../interop/header_main_conflict/native.h | 1 + .../header_main_conflict_missing/main.bro | 3 + .../header_main_conflict_missing/native.h | 1 + .../interop/header_symbol_conflict/function.h | 1 + .../interop/header_symbol_conflict/main.bro | 7 + .../interop/header_symbol_conflict/variable.h | 1 + examples/interop/header_unsupported/main.bro | 18 +- .../interop/header_write_conflict/main.bro | 5 + .../interop/header_write_conflict/native.h | 1 + 33 files changed, 3136 insertions(+), 116 deletions(-) create mode 100644 examples/interop/header/include/guarded.h create mode 100644 examples/interop/header/include/once.h create mode 100644 examples/interop/header/include/repeat.h create mode 100644 examples/interop/header_conflict/first.h create mode 100644 examples/interop/header_conflict/main.bro create mode 100644 examples/interop/header_conflict/second.h create mode 100644 examples/interop/header_main_conflict/main.bro create mode 100644 examples/interop/header_main_conflict/native.h create mode 100644 examples/interop/header_main_conflict_missing/main.bro create mode 100644 examples/interop/header_main_conflict_missing/native.h create mode 100644 examples/interop/header_symbol_conflict/function.h create mode 100644 examples/interop/header_symbol_conflict/main.bro create mode 100644 examples/interop/header_symbol_conflict/variable.h create mode 100644 examples/interop/header_write_conflict/main.bro create mode 100644 examples/interop/header_write_conflict/native.h diff --git a/TODO.md b/TODO.md index 237ef0f..2067508 100644 --- a/TODO.md +++ b/TODO.md @@ -5,70 +5,163 @@ # milestones 1. interop type foundation (implemented) - - unsigned integers, floats, and target-dependent c scalar types - - atomic `c_*` primitive types remain distinct until target-aware lowering - - `c_func` and pointer-only `c_struct`; `c` remains an ordinary identifier - - keep binding mutability (`::` / `=`) separate from element or pointee mutability (`mut`) - - arrays and indexing - - `[N]T`: array with `N` logical elements - - `[N;S]T`: array with `N` logical elements followed by sentinel `S` - - pointers - - `@T` / `@mut T`: non-null single-item pointer without arithmetic - - `*T` / `*mut T`: non-null many-item pointer with arithmetic - - optional pointers represent nullable pointers (i.e. `?@T` / `?@mut T`, `?*T` / `?*mut T`) - - slices and slicing - - `[]T`: pointer and length - - `[;S]T`: pointer and length with a sentinel invariant - - ordinary slices do not guarantee null termination - - string literals as immutable sentinel slices backed by static arrays (superseded by milestone 3.5) - - character literals - - optionals with trapping unwrap and fallback operations - - native structs with compiler-controlled layout - - pointer-only `c_struct` support with target c layout - - `Some :: c_struct { ... }`: defined c-layout struct - - `Some :: c_struct`: opaque c-layout struct - - passing c structs by value was deferred until milestone 4.1 + - unsigned integers, floats, and target-dependent c scalar types + - atomic `c_*` primitive types remain distinct until target-aware lowering + - `c_func` and pointer-only `c_struct`; `c` remains an ordinary identifier + - keep binding mutability (`::` / `=`) separate from element or pointee mutability (`mut`) + - arrays and indexing + - `[N]T`: array with `N` logical elements + - `[N;S]T`: array with `N` logical elements followed by sentinel `S` + - pointers + - `@T` / `@mut T`: non-null single-item pointer without arithmetic + - `*T` / `*mut T`: non-null many-item pointer with arithmetic + - optional pointers represent nullable pointers (i.e. `?@T` / `?@mut T`, `?*T` / `?*mut T`) + - slices and slicing + - `[]T`: pointer and length + - `[;S]T`: pointer and length with a sentinel invariant + - ordinary slices do not guarantee null termination + - string literals as immutable sentinel slices backed by static arrays (superseded by milestone 3.5) + - character literals + - optionals with trapping unwrap and fallback operations + - native structs with compiler-controlled layout + - pointer-only `c_struct` support with target c layout + - `Some :: c_struct { ... }`: defined c-layout struct + - `Some :: c_struct`: opaque c-layout struct + - passing c structs by value was deferred until milestone 4.1 2. restricted c header imports (implemented) - - treat an imported header as a synthetic, file-local package namespace - - `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 - - dynamically load libclang behind a replaceable c importer boundary + - treat an imported header as a synthetic, file-local package namespace + - `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 + - dynamically load libclang behind a replaceable c importer boundary 3. c variadic calls (implemented) - - 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 + - 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 3.5. sentinel pointers and c strings (implemented) - - add sentinel many-item pointers: `[*;S]T` - - represent string literals as immutable pointers to statically stored sentinel arrays: `@[N;0]u8` - - arrays expose `.len` but no `.ptr`; slices and pointers-to-arrays expose sentinel-preserving `.ptr` - - allow pointer-to-array `.len`, indexing, slicing, pointer decay, and slice construction without explicit dereference - - preserve or forget sentinel information through compatible pointer and slice coercions without copying arrays - - allow zero-terminated immutable byte pointer views to convert to immutable `*c_char` and `[*;0]c_char` - - keep `u8` and `c_char` distinct to preserve target-dependent scalar c semantics - - reject general `u8`/`c_char` interchange, slice-to-pointer coercion, and conversion to mutable c character pointers + - add sentinel many-item pointers: `[*;S]T` + - represent string literals as immutable pointers to statically stored sentinel arrays: `@[N;0]u8` + - arrays expose `.len` but no `.ptr`; slices and pointers-to-arrays expose sentinel-preserving `.ptr` + - allow pointer-to-array `.len`, indexing, slicing, pointer decay, and slice construction without explicit dereference + - preserve or forget sentinel information through compatible pointer and slice coercions without copying arrays + - allow zero-terminated immutable byte pointer views to convert to immutable `*c_char` and `[*;0]c_char` + - keep `u8` and `c_char` distinct to preserve target-dependent scalar c semantics + - reject general `u8`/`c_char` interchange, slice-to-pointer coercion, and conversion to mutable c character pointers 4. advanced c interop - - by-value records and unions (implemented) - - complete plain imported structs/unions and manual `c_struct` values - - fixed C arrays inside imported records - - keyed struct literals and exactly-one-field union literals - - field reads/writes, storage, and fixed-signature calls/returns - - aarch64-macos small aggregate, homogeneous float aggregate, and indirect ABI lowering - - keep incomplete, bitfield, packed, flexible-array, qualified-field, and otherwise non-plain records pointer-only - - keep C variadic record arguments unsupported - - function pointers and callbacks (implemented) - - imported C function pointer typedefs lower to nullable pointer types - - manual `?*c_func(...) T` callback type spelling - - concrete `c_func` declarations/definitions can be passed as callback values - - postfix calls through non-null function pointers, including `callback?(...)` - - fixed and C-variadic callback ABI emission through LLVM indirect calls - - external variables - - macros and static inline functions - - exporting brolang functions to c + - by-value records and unions (implemented) + - complete plain imported structs/unions and manual `c_struct` values + - fixed C arrays inside imported records + - keyed struct literals and exactly-one-field union literals + - field reads/writes, storage, and fixed-signature calls/returns + - aarch64-macos small aggregate, homogeneous float aggregate, and indirect ABI lowering + - keep incomplete, bitfield, packed, flexible-array, qualified-field, and otherwise non-plain records pointer-only + - keep C variadic record arguments unsupported + - function pointers and callbacks (implemented) + - imported C function pointer typedefs lower to nullable pointer types + - manual `?*c_func(...) T` callback type spelling + - concrete `c_func` declarations/definitions can be passed as callback values + - postfix calls through non-null function pointers, including `callback?(...)` + - fixed and C-variadic callback ABI emission through LLVM indirect calls + - external variables (implemented) + - imported external C object variables lower to direct LLVM external global references + - top-level `const` object variables are read-only from brolang + - mutable external scalars/records can be assigned through qualified package globals + - unsupported variable types remain lazy diagnostics when referenced + - object-like macro constants (implemented) + - scalar integer/float literal macros import as immutable globals + - `CLITERAL(Type){ ... }` / `(Type){ ... }` record literal macros import as immutable globals + - function-like macros and non-literal macro expressions remain unsupported + - static inline functions (implemented) + +- 5. control flow + - boolean expressions + - operators: `and`, `or`, `!` + - lazy evaluation / short-circuit evaluation + - if statements. example: `if condition { ... } else if { ... } else { ... }` + - conditional unwrapping for optionals (`?T`): `if val |v| { ... } else { ... }` - unwrap `val` into `v` if it is not `none` + - conditional unwrapping with guard clause: `if val |v : v >= 10| { ... } else { ... }` - unwrap `val` into `v` if it is not `none` + - multi-unwrap (see section below) + - while loops (operates on boolean conditions). examples: + - `while condition { ... }` - iterate while the condition is true + - `while condition : i += 1 { ... }` - iterate while the condition is true and execute `i += 1` (continue expression) after each iteration + - ranges (see section below) + - for loops (operates on iterable sequences). examples: + - `for items |item| { ... }` - capture just the `item` value in the array/slice (uses copy semantics, i.e. gets a `T`) + - `for items |&item| { ... }` - capture just the `item` value in the array/slice (uses (immutable) reference semantics, i.e. gets a `@T`) + - `for items |&mut item| { ... }` - capture just the `item` value in the array/slice (uses (mutable) reference semantics, i.e. gets a `@mut T`) + - `for items |item, idx| { ... }` - capture `item` and its index index in the array/slice + - `for 0..10 |i| { ... }` - iterate over the range `0..10` (exclusive) + - `for 0..=10 |i| { ... }` - iterate over the range `0..10` (inclusive) + - `for 0..(len) |i| { ... }` or equivalently `for 0..=(len - 1) |i| { ... }` - calculating range bounds, expressions must be parenthesized + - for all conditionals/guards, parentheses are optional but allowed for visual clarity + +## A word on multi-unwrap + +Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated. + +```honey +name: ?[]u8 = get_name() +age: ?u8 = get_age() +if name and age |n, a| { + # both n and a are guaranteed non-none here + print("{s} is {d} years old", {n, a}) +} +``` + +**With guard clause on multiple values:** + +```honey +if name and hat |n, h : n == "Huginn" and h.brand == .gucci| { + print("{s}'s got that drip\n", {n}) +} +``` + +Parentheses around the expression are optional, but can aid readability when combined with guards: + +```honey +# without parentheses +if name and hat |n, h : guard| { ... } + +# with parentheses for clarity +if (name and hat) |n, h : guard| { ... } +``` + +## A word on lazy / short-circuit evaluation + +The `and` in multi-unwrap short-circuits left-to-right: + +```honey +if get_name() and get_hat() |n, h| { + # get_hat() is only called if get_name() returned non-none +} +``` + +This is important for avoiding unnecessary computation or side effects. + +## A word on ranges + +Ranges represent a sequence of values, commonly used in for loops, and is itself a value type: + +``` +0..10 # exclusive: 0, 1, 2, ..., 9 +0..=10 # inclusive: 0, 1, 2, ..., 10 +``` + +**Parenthesization rule:** Each side of `..` must be either a simple term (literal or identifier) or a parenthesized expression. This eliminates precedence ambiguity: + +``` +0..10 # OK: both sides are literals +0..n # OK: both sides are simple +0..(n + 1) # OK: complex expression is parenthesized +(a + 1)..(b - 1) # OK: both sides parenthesized +# 0..n + 1 # ERROR: must parenthesize complex expressions +``` + +This rule keeps the grammar simple and forces clarity at the call site — no precedence rules to remember. Also, being a value type, ranges can be assigned to variables and passed around like any other value. diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index ccacfac..729bb45 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -134,6 +134,7 @@ Function :: struct { params: []Param, result: Type_Syntax, body: []Stmt_Id, + link_name: string, unsupported_reason: string, diagnostic: source.Diagnostic_Id, } @@ -141,10 +142,13 @@ Function :: struct { Global :: struct { span: source.Span, name: symbol.Id, + link_name: string, pkg: Package_Id, file: File_Id, type: Type_Syntax, immutable: bool, + external: bool, + writable: bool, expr: Expr_Id, diagnostic: source.Diagnostic_Id, } @@ -184,6 +188,16 @@ Unsupported :: struct { reason: string, } +// Trampoline is a generated C wrapper source that must be compiled and linked +// alongside the program so an internal-linkage (`static inline`) C function can +// be called through an external symbol. `source` is the wrapper function only; +// `header` is the absolute path it must `#include` (emitted once per header). +Trampoline :: struct { + symbol: string, + source: string, + header: string, +} + Module :: struct { exprs: [dynamic]Expr, statements: [dynamic]Stmt, @@ -193,6 +207,7 @@ Module :: struct { files: [dynamic]File, packages: [dynamic]Package, unsupported: [dynamic]Unsupported, + c_trampolines: [dynamic]Trampoline, strings: [dynamic]string, type_store: types.Store, allocator: mem.Allocator, @@ -210,6 +225,7 @@ init_module :: proc(allocator := context.allocator) -> Module { module.files.allocator = allocator module.packages.allocator = allocator module.unsupported.allocator = allocator + module.c_trampolines.allocator = allocator module.strings.allocator = allocator return module } @@ -221,17 +237,26 @@ destroy_module :: proc(module: ^Module) { for function in module.functions { delete(function.params, module.allocator) delete(function.body, module.allocator) + delete(function.link_name, module.allocator) delete(function.unsupported_reason, module.allocator) } for import_item in module.imports { delete(import_item.path, module.allocator) } + for global in module.globals { + delete(global.link_name, module.allocator) + } for pkg in module.packages { delete(pkg.path, module.allocator) } for item in module.unsupported { delete(item.reason, module.allocator) } + for trampoline in module.c_trampolines { + delete(trampoline.symbol, module.allocator) + delete(trampoline.source, module.allocator) + delete(trampoline.header, module.allocator) + } for value in module.strings { delete(value, module.allocator) } @@ -243,6 +268,7 @@ destroy_module :: proc(module: ^Module) { delete(module.files) delete(module.packages) delete(module.unsupported) + delete(module.c_trampolines) delete(module.strings) types.destroy_store(&module.type_store) } diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 9793f09..5472e0b 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -84,6 +84,8 @@ Checker :: struct { global_index: []Global_Index_Entry, import_index: []Import_Index_Entry, global_types: []types.Type, + external_global_canonical: []ast.Global_Id, + external_global_diagnostics: []source.Diagnostic_Id, constants: []Constant, template_diagnostics: []source.Diagnostic_Id, constant_stack: [dynamic]Constant_Frame, @@ -600,6 +602,72 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as } } +validate_external_globals :: proc(checker: ^Checker) { + for global, global_index in checker.ast_module.globals { + checker.external_global_canonical[global_index] = ast.global_id(global_index) + if !global.external { + continue + } + switch global.link_name { + case "main": + checker.external_global_diagnostics[global_index] = source.add( + checker.diagnostics, + global.span, + "external C variable 'main' conflicts with the program entry point", + ) + case "write": + checker.external_global_diagnostics[global_index] = source.add( + checker.diagnostics, + global.span, + "external C variable 'write' conflicts with the compiler runtime", + ) + } + for previous, previous_index in checker.ast_module.globals[:global_index] { + if !previous.external || previous.link_name != global.link_name { + continue + } + canonical := checker.external_global_canonical[previous_index] + if canonical == ast.INVALID_GLOBAL { + canonical = ast.global_id(previous_index) + } + canonical_index := int(canonical) + if canonical_index < 0 || canonical_index >= len(checker.ast_module.globals) { + canonical = ast.global_id(previous_index) + canonical_index = previous_index + } + checker.external_global_canonical[global_index] = canonical + canonical_global := checker.ast_module.globals[canonical_index] + canonical_type := checker.global_types[canonical_index] + if !types.equal(checker.global_types[global_index], canonical_type) || + global.writable != canonical_global.writable { + checker.external_global_diagnostics[global_index] = source.addf( + checker.diagnostics, + global.span, + "conflicting external C variable declarations for '%s'", + global.link_name, + ) + } + checker.global_types[global_index] = canonical_type + break + } + for function in checker.ast_module.functions { + if !function.c_abi || function.has_body || len(function.unsupported_reason) > 0 || + symbol_text(checker, function.name) != global.link_name { + continue + } + if checker.external_global_diagnostics[global_index] == source.INVALID_DIAGNOSTIC { + checker.external_global_diagnostics[global_index] = source.addf( + checker.diagnostics, + global.span, + "external C variable '%s' conflicts with a C function declaration", + global.link_name, + ) + } + break + } + } +} + validate_declarations :: proc(checker: ^Checker) { for function, function_id in checker.ast_module.functions { if len(function.unsupported_reason) > 0 { @@ -1362,6 +1430,9 @@ infer_all :: proc(checker: ^Checker) { changed := false spec_count := len(checker.specs) for global, index in checker.ast_module.globals { + if global.external { + continue + } inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file) if is_runtime_type(checker, type_from_syntax(global.type)) { continue @@ -1392,6 +1463,9 @@ prune_specs :: proc(checker: ^Checker) { mark_spec_demanded(checker, find_spec(checker, main_template, nil), &stack) } for global in checker.ast_module.globals { + if global.external { + continue + } _ = infer_expr(checker, global.expr, nil, global.pkg, global.file, &stack) } for len(stack) > 0 { @@ -1449,6 +1523,27 @@ add_unique_global :: proc(values: ^[dynamic]hir.Global_Id, value: hir.Global_Id) append(values, value) } +build_global_reference :: proc( + checker: ^Checker, + global: ast.Global_Id, + span: source.Span, + global_reads: ^[dynamic]hir.Global_Id, +) -> hir.Expr_Id { + if int(global) < len(checker.external_global_diagnostics) { + diagnostic := checker.external_global_diagnostics[global] + if diagnostic != source.INVALID_DIAGNOSTIC { + return invalid_hir_expr(checker, span, diagnostic, checker.global_types[global]) + } + } + hir_global := hir.Global_Id(global) + add_unique_global(global_reads, hir_global) + return add_hir_expr(checker, hir.Expr{ + kind=.Global, span=span, type=checker.global_types[global], + target=hir.global_ref(hir_global), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + add_unique_function :: proc(values: ^[dynamic]hir.Function_Id, value: hir.Function_Id) { for existing in values { if existing == value { @@ -1689,13 +1784,23 @@ hir_location_writable :: proc(checker: ^Checker, expr_id: hir.Expr_Id, locals: [ return local.mutable } } + case .Global: + id := hir.as_global(expr.target) + return id != hir.INVALID_GLOBAL && int(id) < len(checker.module.globals) && + checker.module.globals[id].writable case .Deref: pointer_type := checker.module.exprs[expr.left].type return types.is_mutable(pointer_type, &checker.module.types) case .Index: container_type := checker.module.exprs[expr.left].type item, ok := types.container(container_type, &checker.module.types) - return ok && item.mutable + if !ok || !item.mutable { + return false + } + if types.is_array(container_type, &checker.module.types) { + return hir_location_writable(checker, expr.left, locals) + } + return true case .Field: base_type := checker.module.exprs[expr.left].type if types.is_pointer(base_type, &checker.module.types) { @@ -2188,12 +2293,7 @@ build_expr :: proc( id := add_package_resolution_diagnostic(checker, expr, file) last = invalid_hir_expr(checker, expr.span, id) } else if global := find_global(checker, expr.name, target_pkg); global != ast.INVALID_GLOBAL { - hir_global := hir.Global_Id(global) - add_unique_global(global_reads, hir_global) - last = add_hir_expr(checker, hir.Expr{ - kind=.Global, span=expr.span, type=checker.global_types[global], - target=hir.global_ref(hir_global), left = hir.INVALID_EXPR, right = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, - }) + last = build_global_reference(checker, global, expr.span, global_reads) } else { template := find_template(checker, expr.name, target_pkg) if template != ast.INVALID_FUNCTION && checker.ast_module.functions[template].c_abi { @@ -2241,13 +2341,7 @@ build_expr :: proc( } if callee == hir.INVALID_EXPR { if global := find_global(checker, expr.name, target_pkg); global != ast.INVALID_GLOBAL { - hir_global := hir.Global_Id(global) - add_unique_global(global_reads, hir_global) - callee = add_hir_expr(checker, hir.Expr{ - kind=.Global, span=expr.span, type=checker.global_types[global], - target=hir.global_ref(hir_global), left=hir.INVALID_EXPR, - right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, - }) + callee = build_global_reference(checker, global, expr.span, global_reads) callee_from_global = true } } @@ -2561,6 +2655,9 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string { return fmt.aprintf("main", allocator = checker.allocator) } if !function.has_body && function.c_abi { + if len(function.link_name) > 0 { + return strings.clone(function.link_name, checker.allocator) + } return fmt.aprintf("%s", symbol_text(checker, function.name), allocator = checker.allocator) } builder := strings.builder_make(checker.allocator) @@ -3085,6 +3182,41 @@ expr_problematic :: proc(checker: ^Checker, expr_id: hir.Expr_Id) -> bool { build_globals :: proc(checker: ^Checker) { for global, global_index in checker.ast_module.globals { + if global.external { + global_type := checker.global_types[global_index] + writable := global.writable + canonical := checker.external_global_canonical[global_index] + if canonical != ast.INVALID_GLOBAL && int(canonical) < len(checker.ast_module.globals) { + canonical_global := checker.ast_module.globals[canonical] + writable = canonical_global.writable + global_type = checker.global_types[canonical] + } + diagnostic := checker.external_global_diagnostics[global_index] + if !is_runtime_type(checker, global_type) { + if diagnostic == source.INVALID_DIAGNOSTIC { + diagnostic = source.addf( + checker.diagnostics, + global.span, + "could not resolve a concrete type for external global '%s'", + symbol_text(checker, global.name), + ) + } + global_type = types.I64 + } + _ = hir.global_id(len(checker.module.globals)) + append(&checker.module.globals, hir.Global{ + name=global.name, + link_name=strings.clone(global.link_name, checker.allocator), + type=global_type, + expr=hir.INVALID_EXPR, + external=true, + writable=writable, + direct_problem=diagnostic != source.INVALID_DIAGNOSTIC, + problematic=diagnostic != source.INVALID_DIAGNOSTIC, + diagnostic=diagnostic, + }) + continue + } dependencies: [dynamic]hir.Global_Id dependencies.allocator = checker.allocator calls: [dynamic]hir.Function_Id @@ -3139,10 +3271,13 @@ build_globals :: proc(checker: ^Checker) { &checker.module.globals, hir.Global { name = global.name, + link_name = strings.clone(global.link_name, checker.allocator), type = global_type, expr = expr, static_value = static_value, is_static = is_static, + external = false, + writable = false, dependencies = dependencies, calls = calls[:], direct_problem = expr_problematic(checker, expr), @@ -3386,6 +3521,14 @@ check :: proc( checker.cycle_stack.allocator = allocator build_symbol_indexes(&checker) checker.global_types = make([]types.Type, len(ast_module.globals), allocator) + checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator) + checker.external_global_diagnostics = make([]source.Diagnostic_Id, len(ast_module.globals), allocator) + for &canonical in checker.external_global_canonical { + canonical = ast.INVALID_GLOBAL + } + for &diagnostic in checker.external_global_diagnostics { + diagnostic = source.INVALID_DIAGNOSTIC + } checker.constants = make([]Constant, len(ast_module.exprs), allocator) checker.template_diagnostics = make([]source.Diagnostic_Id, len(ast_module.functions), allocator) for &diagnostic in checker.template_diagnostics { @@ -3400,6 +3543,8 @@ check :: proc( delete(checker.global_index, allocator) delete(checker.import_index, allocator) delete(checker.global_types, allocator) + delete(checker.external_global_canonical, allocator) + delete(checker.external_global_diagnostics, allocator) delete(checker.constants, allocator) delete(checker.template_diagnostics, allocator) delete(checker.constant_stack) @@ -3433,6 +3578,7 @@ check :: proc( validate_type_nodes(&checker) validate_declarations(&checker) infer_all(&checker) + validate_external_globals(&checker) prune_specs(&checker) build_globals(&checker) for index := 0; index < len(checker.specs); index += 1 { diff --git a/compiler/cimport/cimport.odin b/compiler/cimport/cimport.odin index d080006..8a4a1dc 100644 --- a/compiler/cimport/cimport.odin +++ b/compiler/cimport/cimport.odin @@ -68,16 +68,59 @@ Alias :: struct { } Function :: struct { - name: string, - params: []Type_Id, - result: Type_Id, - variadic: bool, - reason: string, + name: string, + params: []Type_Id, + result: Type_Id, + variadic: bool, + link_name: string, + reason: string, +} + +// Trampoline is a generated C wrapper that gives an external symbol to a +// `static inline` (or otherwise internal-linkage) C function so brolang can +// call it. The wrapper is compiled and linked alongside the program. `source` +// is the wrapper function only; `header` is the absolute path the wrapper must +// `#include`, emitted once per unique header by the driver. +Trampoline :: struct { + symbol: string, + source: string, + header: string, +} + +Variable :: struct { + name: string, + type: Type_Id, + mutable: bool, + reason: string, +} + +Macro_Value_Kind :: enum u8 { + Invalid, + Integer, + Float, +} + +Macro_Value :: struct { + kind: Macro_Value_Kind, + type: Type_Id, + integer: u64, + negative: bool, +} + +Macro_Constant :: struct { + name: string, + type: Type_Id, + type_name: string, + value: Macro_Value, + values: []Macro_Value, + aggregate: bool, + reason: string, } Unsupported :: struct { - name: string, - reason: string, + name: string, + reason: string, + final_macro: bool, } Result :: struct { @@ -85,7 +128,10 @@ Result :: struct { records: [dynamic]Record, aliases: [dynamic]Alias, functions: [dynamic]Function, + variables: [dynamic]Variable, + macros: [dynamic]Macro_Constant, unsupported: [dynamic]Unsupported, + trampolines: [dynamic]Trampoline, error_message: string, infrastructure: bool, available: bool, @@ -99,7 +145,10 @@ init_result :: proc(allocator := context.allocator) -> Result { result.records.allocator = allocator result.aliases.allocator = allocator result.functions.allocator = allocator + result.variables.allocator = allocator + result.macros.allocator = allocator result.unsupported.allocator = allocator + result.trampolines.allocator = allocator return result } @@ -123,18 +172,37 @@ destroy_result :: proc(result: ^Result) { for function in result.functions { delete(function.name, result.allocator) delete(function.params, result.allocator) + delete(function.link_name, result.allocator) delete(function.reason, result.allocator) } + for variable in result.variables { + delete(variable.name, result.allocator) + delete(variable.reason, result.allocator) + } + for macro in result.macros { + delete(macro.name, result.allocator) + delete(macro.type_name, result.allocator) + delete(macro.values, result.allocator) + delete(macro.reason, result.allocator) + } for item in result.unsupported { delete(item.name, result.allocator) delete(item.reason, result.allocator) } + for trampoline in result.trampolines { + delete(trampoline.symbol, result.allocator) + delete(trampoline.source, result.allocator) + delete(trampoline.header, result.allocator) + } delete(result.error_message, result.allocator) delete(result.types) delete(result.records) delete(result.aliases) delete(result.functions) + delete(result.variables) + delete(result.macros) delete(result.unsupported) + delete(result.trampolines) } Request :: struct { diff --git a/compiler/cimport/libclang.odin b/compiler/cimport/libclang.odin index 22bf4d5..1b79ab7 100644 --- a/compiler/cimport/libclang.odin +++ b/compiler/cimport/libclang.odin @@ -4,8 +4,11 @@ import "../target" import "base:runtime" import "core:dynlib" import "core:fmt" +import "core:hash" +import "core:math" import "core:mem" import "core:os/os2" +import "core:strconv" import "core:strings" CXCursor :: struct { @@ -24,9 +27,32 @@ CXString :: struct { private_flags: u32, } +CXSourceLocation :: struct { + ptr_data: [2]rawptr, + int_data: u32, +} + +CXSourceRange :: struct { + ptr_data: [2]rawptr, + begin_int_data: u32, + end_int_data: u32, +} + +CXToken :: struct { + int_data: [4]u32, + ptr_data: rawptr, +} + +CXUnsavedFile :: struct { + filename: cstring, + contents: cstring, + length: uint, +} + CXIndex :: distinct rawptr CXTranslationUnit :: distinct rawptr CXDiagnostic :: distinct rawptr +CXFile :: distinct rawptr Cursor_Visitor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 @@ -35,17 +61,24 @@ Api :: struct { 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, + parse_translation_unit: proc "c"(CXIndex, cstring, [^]cstring, i32, [^]CXUnsavedFile, 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_extent: proc "c"(CXCursor) -> CXSourceRange, + get_cursor_location: proc "c"(CXCursor) -> CXSourceLocation, get_cursor_linkage: proc "c"(CXCursor) -> i32, + get_cursor_tls_kind: proc "c"(CXCursor) -> i32, get_cursor_definition: proc "c"(CXCursor) -> CXCursor, is_cursor_definition: proc "c"(CXCursor) -> u32, + get_file: proc "c"(CXTranslationUnit, cstring) -> CXFile, + get_file_contents: proc "c"(CXTranslationUnit, CXFile, ^uint) -> [^]byte, + get_file_location: proc "c"(CXSourceLocation, ^CXFile, ^u32, ^u32, ^u32), get_cursor_type: proc "c"(CXCursor) -> CXType, + get_type_spelling: proc "c"(CXType) -> CXString, get_typedef_underlying_type: proc "c"(CXCursor) -> CXType, get_type_declaration: proc "c"(CXType) -> CXCursor, get_canonical_type: proc "c"(CXType) -> CXType, @@ -58,7 +91,10 @@ Api :: struct { is_function_type_variadic: proc "c"(CXType) -> u32, is_const_qualified_type: proc "c"(CXType) -> u32, is_volatile_qualified_type: proc "c"(CXType) -> u32, + cursor_is_macro_function_like: proc "c"(CXCursor) -> u32, + cursor_is_macro_builtin: proc "c"(CXCursor) -> u32, cursor_is_bitfield: proc "c"(CXCursor) -> u32, + cursor_is_function_inlined: proc "c"(CXCursor) -> u32, cursor_get_offset_of_field: proc "c"(CXCursor) -> i64, type_get_size_of: proc "c"(CXType) -> i64, type_get_align_of: proc "c"(CXType) -> i64, @@ -68,6 +104,9 @@ Api :: struct { get_diagnostic_severity: proc "c"(CXDiagnostic) -> i32, get_diagnostic_spelling: proc "c"(CXDiagnostic) -> CXString, dispose_diagnostic: proc "c"(CXDiagnostic), + tokenize: proc "c"(CXTranslationUnit, CXSourceRange, ^[^]CXToken, ^u32), + get_token_spelling: proc "c"(CXTranslationUnit, CXToken) -> CXString, + dispose_tokens: proc "c"(CXTranslationUnit, [^]CXToken, u32), get_cstring: proc "c"(CXString) -> cstring, dispose_string: proc "c"(CXString), } @@ -82,6 +121,7 @@ CXCursor_MacroDefinition :: i32(501) CXCursor_FieldDecl :: i32(6) CXLinkage_External :: i32(4) +CXTLS_None :: i32(0) CXType_Invalid :: i32(0) CXType_Unexposed :: i32(1) @@ -148,10 +188,17 @@ load_api_from :: proc(path: string) -> (Api, bool) { 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_getCursorExtent", &api.get_cursor_extent) && + load_proc(&api, "clang_getCursorLocation", &api.get_cursor_location) && load_proc(&api, "clang_getCursorLinkage", &api.get_cursor_linkage) && + load_proc(&api, "clang_getCursorTLSKind", &api.get_cursor_tls_kind) && load_proc(&api, "clang_getCursorDefinition", &api.get_cursor_definition) && load_proc(&api, "clang_isCursorDefinition", &api.is_cursor_definition) && + load_proc(&api, "clang_getFile", &api.get_file) && + load_proc(&api, "clang_getFileContents", &api.get_file_contents) && + load_proc(&api, "clang_getFileLocation", &api.get_file_location) && load_proc(&api, "clang_getCursorType", &api.get_cursor_type) && + load_proc(&api, "clang_getTypeSpelling", &api.get_type_spelling) && 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) && @@ -164,7 +211,10 @@ load_api_from :: proc(path: string) -> (Api, bool) { load_proc(&api, "clang_isFunctionTypeVariadic", &api.is_function_type_variadic) && 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_isMacroFunctionLike", &api.cursor_is_macro_function_like) && + load_proc(&api, "clang_Cursor_isMacroBuiltin", &api.cursor_is_macro_builtin) && load_proc(&api, "clang_Cursor_isBitField", &api.cursor_is_bitfield) && + load_proc(&api, "clang_Cursor_isFunctionInlined", &api.cursor_is_function_inlined) && load_proc(&api, "clang_Cursor_getOffsetOfField", &api.cursor_get_offset_of_field) && load_proc(&api, "clang_Type_getSizeOf", &api.type_get_size_of) && load_proc(&api, "clang_Type_getAlignOf", &api.type_get_align_of) && @@ -174,6 +224,9 @@ load_api_from :: proc(path: string) -> (Api, bool) { 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_tokenize", &api.tokenize) && + load_proc(&api, "clang_getTokenSpelling", &api.get_token_spelling) && + load_proc(&api, "clang_disposeTokens", &api.dispose_tokens) && load_proc(&api, "clang_getCString", &api.get_cstring) && load_proc(&api, "clang_disposeString", &api.dispose_string) if !ok { @@ -214,12 +267,28 @@ clone_cx_string :: proc(api: ^Api, value: CXString, allocator: mem.Allocator) -> return fmt.aprintf("%s", string(text), allocator=allocator) } -Context :: struct { - api: ^Api, - result: ^Result, +Macro_State :: struct { + name: string, + defined: bool, +} + +Macro_Table :: struct { + items: [dynamic]Macro_State, + lookup: map[string]int, allocator: mem.Allocator, } +Context :: struct { + api: ^Api, + translation_unit: CXTranslationUnit, + result: ^Result, + final_macros: ^Macro_Table, + variable_lookup: map[string]int, + allocator: mem.Allocator, + target: target.Target, + header_path: string, +} + add_type :: proc(ctx: ^Context, value: Type) -> Type_Id { id := Type_Id(len(ctx.result.types)) append(&ctx.result.types, value) @@ -382,11 +451,18 @@ translate_type :: proc(ctx: ^Context, value: CXType, preferred_record_name := "" }) case CXType_ConstantArray: count := ctx.api.get_array_size(value) - child := translate_type(ctx, ctx.api.get_array_element_type(value), "", depth+1) + element := ctx.api.get_array_element_type(value) + child := translate_type(ctx, element, "", depth+1) if count <= 0 || child == INVALID_TYPE { return INVALID_TYPE } - return add_type(ctx, Type{kind=.Array, child=child, count=u64(count), mutable=true}) + return add_type(ctx, Type{ + kind=.Array, + child=child, + count=u64(count), + mutable=ctx.api.is_const_qualified_type(value) == 0 && + ctx.api.is_const_qualified_type(element) == 0, + }) case CXType_Record: declaration := ctx.api.get_type_declaration(value) record := add_record(ctx, declaration, preferred_record_name) @@ -418,6 +494,10 @@ translate_type :: proc(ctx: ^Context, value: CXType, preferred_record_name := "" variadic=ctx.api.is_function_type_variadic(value) != 0, }) case CXType_Typedef: + canonical := ctx.api.get_canonical_type(value) + if canonical.kind != CXType_Invalid && canonical.kind != value.kind { + return translate_type(ctx, canonical, preferred_record_name, depth+1) + } 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) @@ -444,13 +524,14 @@ has_named :: proc(items: []Unsupported, name: string) -> bool { return false } -add_unsupported :: proc(ctx: ^Context, name, reason: string) { +add_unsupported :: proc(ctx: ^Context, name, reason: string, final_macro := false) { 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), + final_macro=final_macro, }) } @@ -470,6 +551,746 @@ add_alias :: proc(ctx: ^Context, name: string, value: Type_Id, reason := "") { }) } +is_macro_identifier :: proc(value: string) -> bool { + if len(value) == 0 { + return false + } + is_start := proc(value: byte) -> bool { + return value == '_' || value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' + } + if !is_start(value[0]) { + return false + } + for byte_value in transmute([]byte)value[1:] { + if !is_start(byte_value) && !(byte_value >= '0' && byte_value <= '9') { + return false + } + } + return true +} + +init_macro_table :: proc(allocator: mem.Allocator) -> Macro_Table { + result: Macro_Table + result.items.allocator = allocator + result.lookup.allocator = allocator + result.allocator = allocator + return result +} + +destroy_macro_table :: proc(table: ^Macro_Table) { + delete(table.lookup) + for item in table.items { + delete(item.name, table.allocator) + } + delete(table.items) +} + +add_macro_candidate :: proc(table: ^Macro_Table, name: string) -> int { + if index, ok := table.lookup[name]; ok { + return index + } + cloned := fmt.aprintf("%s", name, allocator=table.allocator) + index := len(table.items) + append(&table.items, Macro_State{name=cloned}) + table.lookup[cloned] = index + return index +} + +macro_state_defined :: proc(table: ^Macro_Table, name: string) -> bool { + index, ok := table.lookup[name] + return ok && index >= 0 && index < len(table.items) && table.items[index].defined +} + +destroy_macro_constant :: proc(item: Macro_Constant, allocator: mem.Allocator) { + delete(item.name, allocator) + delete(item.type_name, allocator) + delete(item.values, allocator) + delete(item.reason, allocator) +} + +remove_macro_constant :: proc(ctx: ^Context, name: string) { + index := 0 + for index < len(ctx.result.macros) { + if ctx.result.macros[index].name == name { + destroy_macro_constant(ctx.result.macros[index], ctx.allocator) + ordered_remove(&ctx.result.macros, index) + continue + } + index += 1 + } +} + +remove_unsupported :: proc(ctx: ^Context, name: string) { + index := 0 + for index < len(ctx.result.unsupported) { + if ctx.result.unsupported[index].name == name { + delete(ctx.result.unsupported[index].name, ctx.allocator) + delete(ctx.result.unsupported[index].reason, ctx.allocator) + ordered_remove(&ctx.result.unsupported, index) + continue + } + index += 1 + } +} + +clear_macro_import :: proc(ctx: ^Context, name: string) { + remove_macro_constant(ctx, name) + remove_unsupported(ctx, name) +} + +macro_c_primitive :: proc(kind: Type_Kind) -> (target.C_Primitive, bool) { + #partial switch kind { + case .C_Int: return .Int, true + case .C_Uint: return .Uint, true + case .C_Long: return .Long, true + case .C_Ulong: return .Ulong, true + case .C_Longlong: return .Longlong, true + case .C_Ulonglong: return .Ulonglong, true + case: + } + return {}, false +} + +macro_integer_bits_signed :: proc(kind: Type_Kind, selected: target.Target) -> (int, bool, bool) { + primitive, ok := macro_c_primitive(kind) + if !ok { + return 0, false, false + } + layout := target.c_primitive_layout(selected, primitive) + return layout.bits, layout.kind == .Signed_Integer, true +} + +macro_unsigned_max :: proc(bits: int) -> u64 { + if bits >= 64 { + return max(u64) + } + return (u64(1) << u32(bits)) - 1 +} + +macro_signed_max :: proc(bits: int) -> u64 { + return (u64(1) << u32(bits-1)) - 1 +} + +macro_type_fits :: proc(kind: Type_Kind, magnitude: u64, selected: target.Target) -> bool { + bits, signed, ok := macro_integer_bits_signed(kind, selected) + if !ok || bits <= 0 || bits > 64 { + return false + } + if signed { + return magnitude <= macro_signed_max(bits) + } + return magnitude <= macro_unsigned_max(bits) +} + +wrap_negative_unsigned_macro :: proc(magnitude: u64, bits: int) -> u64 { + if magnitude == 0 { + return 0 + } + return ((max(u64)-magnitude)+1) & macro_unsigned_max(bits) +} + +select_macro_integer :: proc( + ctx: ^Context, + magnitude: u64, + negative: bool, + unsigned_suffix: bool, + long_count: int, + nondecimal: bool, +) -> (Macro_Value, Type_Id, bool) { + candidates: []Type_Kind + if unsigned_suffix { + if long_count >= 2 { + candidates = []Type_Kind{.C_Ulonglong} + } else if long_count == 1 { + candidates = []Type_Kind{.C_Ulong, .C_Ulonglong} + } else { + candidates = []Type_Kind{.C_Uint, .C_Ulong, .C_Ulonglong} + } + } else if long_count >= 2 { + if nondecimal { + candidates = []Type_Kind{.C_Longlong, .C_Ulonglong} + } else { + candidates = []Type_Kind{.C_Longlong} + } + } else if long_count == 1 { + if nondecimal { + candidates = []Type_Kind{.C_Long, .C_Ulong, .C_Longlong, .C_Ulonglong} + } else { + candidates = []Type_Kind{.C_Long, .C_Longlong} + } + } else if nondecimal { + candidates = []Type_Kind{.C_Int, .C_Uint, .C_Long, .C_Ulong, .C_Longlong, .C_Ulonglong} + } else { + candidates = []Type_Kind{.C_Int, .C_Long, .C_Longlong} + } + + for kind in candidates { + if !macro_type_fits(kind, magnitude, ctx.target) { + continue + } + macro_type := add_type(ctx, Type{kind=kind, child=INVALID_TYPE}) + value := Macro_Value{kind=.Integer, type=macro_type, integer=magnitude} + bits, signed, _ := macro_integer_bits_signed(kind, ctx.target) + if negative { + if signed { + value.negative = true + } else { + value.integer = wrap_negative_unsigned_macro(magnitude, bits) + } + } + return value, macro_type, true + } + return {}, INVALID_TYPE, false +} + +macro_digit_value :: proc(value: byte) -> (u64, bool) { + if value >= '0' && value <= '9' { + return u64(value-'0'), true + } + if value >= 'a' && value <= 'f' { + return u64(value-'a') + 10, true + } + if value >= 'A' && value <= 'F' { + return u64(value-'A') + 10, true + } + return 0, false +} + +parse_macro_integer_literal :: proc(text: string) -> ( + magnitude: u64, + unsigned_suffix: bool, + long_count: int, + nondecimal: bool, + ok: bool, +) { + if len(text) == 0 { + return + } + base := u64(10) + start := 0 + if len(text) > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X') { + base = 16 + start = 2 + nondecimal = true + } else if len(text) > 2 && text[0] == '0' && (text[1] == 'b' || text[1] == 'B') { + base = 2 + start = 2 + nondecimal = true + } else if len(text) > 0 && text[0] == '0' { + base = 8 + nondecimal = true + } + if start >= len(text) { + return + } + index := start + for index < len(text) { + digit, digit_ok := macro_digit_value(text[index]) + if !digit_ok || digit >= base { + break + } + if magnitude > (max(u64)-digit)/base { + return + } + magnitude = magnitude*base + digit + index += 1 + } + if index == start { + return + } + for index < len(text) { + byte := text[index] + if byte == 'u' || byte == 'U' { + if unsigned_suffix { + return + } + unsigned_suffix = true + index += 1 + continue + } + if byte == 'l' || byte == 'L' { + if long_count != 0 { + return + } + long_count = 1 + index += 1 + if index < len(text) && (text[index] == 'l' || text[index] == 'L') { + long_count = 2 + index += 1 + } + continue + } + return + } + ok = true + return +} + +strip_float_suffix :: proc(text: string) -> string { + if len(text) > 0 { + last := text[len(text)-1] + if last == 'f' || last == 'F' || last == 'l' || last == 'L' { + return text[:len(text)-1] + } + } + return text +} + +macro_float_kind :: proc(text: string) -> Type_Kind { + if len(text) == 0 { + return .C_Double + } + switch text[len(text)-1] { + case 'f', 'F': + return .C_Float + case 'l', 'L': + return .C_Longdouble + case: + return .C_Double + } + return .C_Double +} + +parse_macro_scalar :: proc(ctx: ^Context, texts: []string) -> (Macro_Value, Type_Id, bool) { + negative := false + literal := "" + if len(texts) == 1 { + literal = texts[0] + } else if len(texts) == 2 && (texts[0] == "-" || texts[0] == "+") { + negative = texts[0] == "-" + literal = texts[1] + } else { + return {}, INVALID_TYPE, false + } + magnitude, unsigned_suffix, long_count, nondecimal, literal_ok := parse_macro_integer_literal(literal) + if literal_ok { + return select_macro_integer(ctx, magnitude, negative, unsigned_suffix, long_count, nondecimal) + } + if strings.contains(literal, ".") || strings.contains(literal, "e") || strings.contains(literal, "E") || + strings.contains(literal, "p") || strings.contains(literal, "P") { + number, ok := strconv.parse_f64(strip_float_suffix(literal)) + if !ok { + return {}, INVALID_TYPE, false + } + if negative { + number = -number + } + kind := macro_float_kind(literal) + stored := number + if kind == .C_Float { + stored = f64(f32(number)) + } + if math.is_nan(stored) || math.is_inf(stored) { + return {}, INVALID_TYPE, false + } + macro_type := add_type(ctx, Type{kind=kind, child=INVALID_TYPE}) + return Macro_Value{ + kind=.Float, + type=macro_type, + integer=transmute(u64)stored, + }, macro_type, true + } + return {}, INVALID_TYPE, false +} + +parse_macro_value_list :: proc(ctx: ^Context, texts: []string, values: ^[dynamic]Macro_Value) -> bool { + index := 0 + for index < len(texts) { + if texts[index] == "," { + index += 1 + continue + } + start := index + for index < len(texts) && texts[index] != "," { + index += 1 + } + if start == index { + return false + } + value, _, ok := parse_macro_scalar(ctx, texts[start:index]) + if !ok { + return false + } + append(values, value) + } + return true +} + +parse_macro_aggregate :: proc(ctx: ^Context, name: string, texts: []string) -> (Macro_Constant, bool) { + type_name := "" + open_index := -1 + if len(texts) >= 6 && texts[0] == "CLITERAL" && texts[1] == "(" && texts[3] == ")" && texts[4] == "{" { + type_name = texts[2] + open_index = 4 + } else if len(texts) >= 5 && texts[0] == "(" && texts[2] == ")" && texts[3] == "{" { + type_name = texts[1] + open_index = 3 + } + if open_index < 0 || !is_macro_identifier(type_name) || texts[len(texts)-1] != "}" { + return {}, false + } + values: [dynamic]Macro_Value + values.allocator = ctx.allocator + if !parse_macro_value_list(ctx, texts[open_index+1:len(texts)-1], &values) { + delete(values) + return {}, false + } + return Macro_Constant{ + name=fmt.aprintf("%s", name, allocator=ctx.allocator), + type_name=fmt.aprintf("%s", type_name, allocator=ctx.allocator), + values=values[:], + aggregate=true, + reason=fmt.aprintf("", allocator=ctx.allocator), + }, true +} + +import_macro_constant :: proc(ctx: ^Context, cursor: CXCursor, name: string) -> bool { + if len(name) == 0 || strings.has_prefix(name, "__") || + !macro_state_defined(ctx.final_macros, name) || ctx.api.cursor_is_macro_builtin(cursor) != 0 { + return false + } + clear_macro_import(ctx, name) + if ctx.api.cursor_is_macro_function_like(cursor) != 0 { + add_unsupported(ctx, name, "C function-like macros are not supported", true) + return true + } + tokens: [^]CXToken + token_count: u32 + ctx.api.tokenize(ctx.translation_unit, ctx.api.get_cursor_extent(cursor), &tokens, &token_count) + defer { + if token_count > 0 { + ctx.api.dispose_tokens(ctx.translation_unit, tokens, token_count) + } + } + if token_count <= 1 { + add_unsupported(ctx, name, "C macro has no replacement value", true) + return true + } + texts := make([]string, int(token_count), ctx.allocator) + defer { + for text in texts { + delete(text, ctx.allocator) + } + delete(texts, ctx.allocator) + } + for index := 0; index < int(token_count); index += 1 { + texts[index] = clone_cx_string(ctx.api, ctx.api.get_token_spelling(ctx.translation_unit, tokens[index]), ctx.allocator) + } + replacement := texts[1:] + if value, ok := parse_macro_aggregate(ctx, name, replacement); ok { + append(&ctx.result.macros, value) + return true + } + if value, macro_type, ok := parse_macro_scalar(ctx, replacement); ok { + append(&ctx.result.macros, Macro_Constant{ + name=fmt.aprintf("%s", name, allocator=ctx.allocator), + type=macro_type, + value=value, + reason=fmt.aprintf("", allocator=ctx.allocator), + }) + return true + } + add_unsupported(ctx, name, "C macro is not a supported constant", true) + return true +} + +cx_file_valid :: proc(file: CXFile) -> bool { + return rawptr(file) != nil +} + +cursor_source_file_offset :: proc(api: ^Api, cursor: CXCursor) -> (CXFile, u32) { + file: CXFile + line, column, offset: u32 + api.get_file_location(api.get_cursor_location(cursor), &file, &line, &column, &offset) + return file, offset +} + +Macro_Collect_Context :: struct { + api: ^Api, + candidates: ^Macro_Table, +} + +visit_macro_candidate :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { + context = runtime.default_context() + ctx := (^Macro_Collect_Context)(client_data) + if ctx.api.get_cursor_kind(cursor) != CXCursor_MacroDefinition || + ctx.api.cursor_is_macro_builtin(cursor) != 0 { + return CXChildVisit_Continue + } + file, _ := cursor_source_file_offset(ctx.api, cursor) + if !cx_file_valid(file) { + return CXChildVisit_Continue + } + name := clone_cx_string(ctx.api, ctx.api.get_cursor_spelling(cursor), ctx.candidates.allocator) + defer delete(name, ctx.candidates.allocator) + if is_macro_identifier(name) && !strings.has_prefix(name, "__") { + _ = add_macro_candidate(ctx.candidates, name) + } + return CXChildVisit_Continue +} + +MACRO_PROBE_PREFIX :: "__BROLANG_FINAL_MACRO_" + +build_macro_probe_source :: proc( + api: ^Api, + translation_unit: CXTranslationUnit, + c_path: cstring, + candidates: ^Macro_Table, + allocator: mem.Allocator, +) -> (string, u32, bool) { + root_file := api.get_file(translation_unit, c_path) + if !cx_file_valid(root_file) { + return "", 0, false + } + size: uint + contents := api.get_file_contents(translation_unit, root_file, &size) + if contents == nil || size > uint(max(u32)) || size > uint(max(int)) { + return "", 0, false + } + builder := strings.builder_make(allocator) + defer strings.builder_destroy(&builder) + if strings.write_bytes(&builder, contents[:int(size)]) != int(size) { + return "", 0, false + } + for candidate, index in candidates.items { + fmt.sbprintf( + &builder, + "\n#if defined(%s)\n#define %s%d 1\n#endif\n", + candidate.name, + MACRO_PROBE_PREFIX, + index, + ) + } + return fmt.aprintf("%s", strings.to_string(builder), allocator=allocator), u32(size), true +} + +Macro_Probe_Context :: struct { + api: ^Api, + candidates: ^Macro_Table, + root_file: CXFile, + probe_start: u32, +} + +visit_macro_probe :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { + context = runtime.default_context() + ctx := (^Macro_Probe_Context)(client_data) + if ctx.api.get_cursor_kind(cursor) != CXCursor_MacroDefinition { + return CXChildVisit_Continue + } + file, offset := cursor_source_file_offset(ctx.api, cursor) + if file != ctx.root_file || offset < ctx.probe_start { + return CXChildVisit_Continue + } + name := clone_cx_string(ctx.api, ctx.api.get_cursor_spelling(cursor), context.temp_allocator) + if !strings.has_prefix(name, MACRO_PROBE_PREFIX) { + return CXChildVisit_Continue + } + index, ok := strconv.parse_uint(name[len(MACRO_PROBE_PREFIX):]) + if ok && index < uint(len(ctx.candidates.items)) { + ctx.candidates.items[index].defined = true + } + return CXChildVisit_Continue +} + +translation_unit_error :: proc(api: ^Api, translation_unit: CXTranslationUnit, allocator: mem.Allocator) -> string { + for diagnostic_index in 0..= CXDiagnostic_Error { + message := clone_cx_string(api, api.get_diagnostic_spelling(diagnostic), allocator) + api.dispose_diagnostic(diagnostic) + return message + } + api.dispose_diagnostic(diagnostic) + } + return "" +} + +probe_final_macros :: proc( + api: ^Api, + index: CXIndex, + translation_unit: CXTranslationUnit, + c_path: cstring, + c_arguments: []cstring, + path: string, + candidates: ^Macro_Table, + allocator: mem.Allocator, +) -> (string, bool) { + if len(candidates.items) == 0 { + return "", true + } + probe_source, probe_start, source_ok := build_macro_probe_source( + api, translation_unit, c_path, candidates, allocator, + ) + if !source_ok { + return fmt.aprintf("libclang could not read header '%s' for macro probing", path, allocator=allocator), false + } + defer delete(probe_source, allocator) + probe_contents := strings.clone_to_cstring(probe_source, context.temp_allocator) + unsaved_file := CXUnsavedFile{ + filename=c_path, + contents=probe_contents, + length=uint(len(probe_source)), + } + probe_translation_unit: CXTranslationUnit + error_code := api.parse_translation_unit( + index, + c_path, + raw_data(c_arguments), + i32(len(c_arguments)), + &unsaved_file, + 1, + CXTranslationUnit_DetailedPreprocessingRecord | CXTranslationUnit_SkipFunctionBodies | + CXTranslationUnit_KeepGoing, + &probe_translation_unit, + ) + if error_code != 0 || probe_translation_unit == nil { + return fmt.aprintf("libclang could not probe final macros for header '%s'", path, allocator=allocator), false + } + defer api.dispose_translation_unit(probe_translation_unit) + if message := translation_unit_error(api, probe_translation_unit, allocator); len(message) > 0 { + return message, false + } + probe_ctx := Macro_Probe_Context{ + api=api, + candidates=candidates, + root_file=api.get_file(probe_translation_unit, c_path), + probe_start=probe_start, + } + _ = api.visit_children(api.get_translation_unit_cursor(probe_translation_unit), visit_macro_probe, &probe_ctx) + return "", true +} + +c_variable_writable :: proc(api: ^Api, value: CXType, depth := 0) -> bool { + if depth > 64 || value.kind == CXType_Invalid || api.is_const_qualified_type(value) != 0 { + return false + } + canonical := api.get_canonical_type(value) + if canonical.kind != CXType_Invalid && api.is_const_qualified_type(canonical) != 0 { + return false + } + if value.kind == CXType_ConstantArray { + return c_variable_writable(api, api.get_array_element_type(value), depth+1) + } + if canonical.kind == CXType_ConstantArray { + return c_variable_writable(api, api.get_array_element_type(canonical), depth+1) + } + return true +} + +add_or_upgrade_variable :: proc( + ctx: ^Context, + name: string, + variable_type: Type_Id, + mutable: bool, + reason: string, +) { + if index, found := ctx.variable_lookup[name]; found { + previous := &ctx.result.variables[index] + if len(previous.reason) > 0 && len(reason) == 0 && variable_type != INVALID_TYPE { + previous.type = variable_type + previous.mutable = mutable + delete(previous.reason, ctx.allocator) + previous.reason = fmt.aprintf("", allocator=ctx.allocator) + } + return + } + index := len(ctx.result.variables) + append(&ctx.result.variables, Variable{ + name=fmt.aprintf("%s", name, allocator=ctx.allocator), + type=variable_type, + mutable=mutable, + reason=fmt.aprintf("%s", reason, allocator=ctx.allocator), + }) + ctx.variable_lookup[ctx.result.variables[index].name] = index +} + +INLINE_TRAMPOLINE_PREFIX :: "__brolang_inline_" + +// declarator_safe reports whether ` name` is a valid C declarator. +// Plain identifiers, pointers, and records satisfy this; function pointers and +// arrays that are not hidden behind a typedef embed the name inside their +// spelling (e.g. `int (*)(int)`, `int[2]`) and are rejected so we never emit a +// malformed wrapper. This is purely a limitation of the simple forwarder, not +// the type system: brolang itself can represent these types, so such a +// `static inline` is reported as unsupported rather than wrapped. +declarator_safe :: proc(spelling: string) -> bool { + return len(spelling) > 0 && + !strings.contains(spelling, "(") && + !strings.contains(spelling, "[") +} + +// build_inline_trampoline synthesizes an external C wrapper that forwards to an +// internal-linkage (typically `static inline`) C function, returning the wrapper +// symbol and recording its source on the result. It fails when any parameter or +// result type needs a complex declarator the simple forwarder cannot express. +build_inline_trampoline :: proc( + ctx: ^Context, + name: string, + function_type: CXType, +) -> (string, bool) { + result_type := ctx.api.get_result_type(function_type) + result_spelling := clone_cx_string(ctx.api, ctx.api.get_type_spelling(result_type), context.temp_allocator) + if !declarator_safe(result_spelling) { + return "", false + } + count := ctx.api.get_num_arg_types(function_type) + if count < 0 { + return "", false + } + param_spellings := make([]string, int(count), context.temp_allocator) + for index in 0.. 0 { + strings.write_string(&builder, ", ") + } + fmt.sbprintf(&builder, "%s a%d", spelling, index) + } + } + strings.write_string(&builder, ") { ") + is_void := ctx.api.get_canonical_type(result_type).kind == CXType_Void + if !is_void { + strings.write_string(&builder, "return ") + } + fmt.sbprintf(&builder, "%s(", name) + for index in 0.. 0 { + strings.write_string(&builder, ", ") + } + fmt.sbprintf(&builder, "a%d", index) + } + strings.write_string(&builder, "); }\n") + + append(&ctx.result.trampolines, Trampoline{ + symbol=symbol, + source=fmt.aprintf("%s", strings.to_string(builder), allocator=ctx.allocator), + header=fmt.aprintf("%s", ctx.header_path, allocator=ctx.allocator), + }) + // `symbol` is owned by the trampoline record above; the caller clones it for + // the function's link name. + return symbol, true +} + visit_cursor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { context = runtime.default_context() ctx := (^Context)(client_data) @@ -482,14 +1303,11 @@ visit_cursor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { 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" - } + link_name := "" variadic := ctx.api.cursor_is_variadic(cursor) != 0 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 { + if result_type == INVALID_TYPE { reason = "function result type is not supported" } params: [dynamic]Type_Id @@ -506,11 +1324,32 @@ visit_cursor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { } } } + linkage := ctx.api.get_cursor_linkage(cursor) + if linkage != CXLinkage_External && len(reason) == 0 { + // Internal-linkage functions have no external symbol. We can still + // call a `static inline` function by linking a generated wrapper. + // `isFunctionInlined` is set from the `inline` keyword without parsing + // the body, so this works under CXTranslationUnit_SkipFunctionBodies. + if variadic { + reason = "variadic static inline C functions are not supported" + } else if ctx.api.cursor_is_function_inlined(cursor) != 0 { + if symbol, ok := build_inline_trampoline(ctx, name, function_type); ok { + link_name = symbol + } else { + reason = "static inline C function has an unsupported parameter or result declarator" + } + } else { + reason = "static and non-external C functions are not supported" + } + } else if linkage != CXLinkage_External { + reason = "static and non-external C functions are not supported" + } append(&ctx.result.functions, Function{ name=fmt.aprintf("%s", name, allocator=ctx.allocator), params=params[:], result=result_type, variadic=variadic, + link_name=fmt.aprintf("%s", link_name, allocator=ctx.allocator), reason=fmt.aprintf("%s", reason, allocator=ctx.allocator), }) case CXCursor_TypedefDecl: @@ -530,9 +1369,30 @@ visit_cursor :: proc "c"(cursor, parent: CXCursor, client_data: rawptr) -> i32 { 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") + if len(name) == 0 { + return CXChildVisit_Continue + } + reason := "" + linkage := ctx.api.get_cursor_linkage(cursor) + if ctx.api.get_cursor_tls_kind(cursor) != CXTLS_None { + reason = "thread-local C variables are not supported" + } else if linkage != CXLinkage_External { + reason = "static and non-external C variables are not supported" + } + variable_type := ctx.api.get_cursor_type(cursor) + translated := translate_type(ctx, variable_type) + if translated == INVALID_TYPE && len(reason) == 0 { + reason = "C variable type is not supported" + } + add_or_upgrade_variable( + ctx, + name, + translated, + c_variable_writable(ctx.api, variable_type), + reason, + ) case CXCursor_MacroDefinition: - add_unsupported(ctx, name, "C macros are not supported") + _ = import_macro_constant(ctx, cursor, name) case: } return CXChildVisit_Continue @@ -590,20 +1450,44 @@ import_with_libclang :: proc(_: rawptr, request: Request, allocator: mem.Allocat } 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 { + if message := translation_unit_error(&api, translation_unit, allocator); len(message) > 0 { + result.error_message = message return result } - ctx := Context{api=&api, result=&result, allocator=allocator} root := api.get_translation_unit_cursor(translation_unit) + final_macros := init_macro_table(allocator) + defer destroy_macro_table(&final_macros) + collect_ctx := Macro_Collect_Context{ + api=&api, + candidates=&final_macros, + } + _ = api.visit_children(root, visit_macro_candidate, &collect_ctx) + if message, ok := probe_final_macros( + &api, + index, + translation_unit, + c_path, + c_arguments, + request.path, + &final_macros, + allocator, + ); !ok { + result.error_message = message + return result + } + + ctx := Context{ + api=&api, + translation_unit=translation_unit, + result=&result, + final_macros=&final_macros, + allocator=allocator, + target=request.target, + header_path=request.path, + } + ctx.variable_lookup.allocator = allocator + defer delete(ctx.variable_lookup) _ = api.visit_children(root, visit_cursor, &ctx) result.available = true return result diff --git a/compiler/compiler.odin b/compiler/compiler.odin index a12a76c..b9bee4e 100644 --- a/compiler/compiler.odin +++ b/compiler/compiler.odin @@ -15,6 +15,18 @@ import "core:fmt" import vmem "core:mem/virtual" import "core:os" import "core:os/os2" +import "core:strings" + +// write_escaped_c_string writes value into builder with `\` and `"` escaped so +// it is safe to embed inside a C string literal (e.g. an `#include "..."`). +write_escaped_c_string :: proc(builder: ^strings.Builder, value: string) { + for b in transmute([]byte)value { + if b == '\\' || b == '"' { + strings.write_byte(builder, '\\') + } + strings.write_byte(builder, b) + } +} compile_package :: proc( input_path, output_path: string, @@ -69,6 +81,54 @@ compile_package :: proc( fmt.eprintln("failed to load root package directory:", input_path) return 2 } + + // Generated C trampolines (for `static inline` imports) must be compiled and + // linked with the program. Write them out and add the source as a link input + // before the loader's arena (which owns the trampoline strings) is freed. + trampoline_path := "" + effective_link_arguments := link_arguments + owns_arguments := false + // Register the path defer first so it runs last (LIFO): the augmented + // argument slice, whose Input value aliases trampoline_path, is freed before + // the path string it points at. + defer if len(trampoline_path) > 0 { + _ = os.remove(trampoline_path) + delete(trampoline_path) + } + defer if owns_arguments { + delete(effective_link_arguments) + } + if len(ast_module.c_trampolines) > 0 { + builder := strings.builder_make() + defer strings.builder_destroy(&builder) + seen_headers: map[string]bool + defer delete(seen_headers) + for trampoline in ast_module.c_trampolines { + if seen_headers[trampoline.header] { + continue + } + seen_headers[trampoline.header] = true + strings.write_string(&builder, "#include \"") + write_escaped_c_string(&builder, trampoline.header) + strings.write_string(&builder, "\"\n") + } + for trampoline in ast_module.c_trampolines { + strings.write_string(&builder, trampoline.source) + } + // Owned (stable allocator); freed in the path defer above after backend + // compilation, which is the only consumer, has run. + trampoline_path = fmt.aprintf("%s.brolang-trampolines-%d.c", output_path, os2.get_pid()) + if err := os.write_entire_file_or_err(trampoline_path, transmute([]byte)strings.to_string(builder)); err != nil { + fmt.eprintln("failed to write C trampoline source:", err) + return 2 + } + augmented := make([]linker.Argument, len(link_arguments)+1) + copy(augmented, link_arguments) + augmented[len(link_arguments)] = linker.Argument{kind=.Input, value=trampoline_path} + effective_link_arguments = augmented + owns_arguments = true + } + vmem.arena_free_all(&lexer_arena) hir_module := checker.check(&ast_module, &diagnostics, &symbols, selected, vmem.arena_allocator(&checker_arena)) vmem.arena_free_all(&parser_arena) @@ -87,7 +147,7 @@ compile_package :: proc( } source.print_all(&diagnostics) - if !backend.compile(llvm_path, output_path, link_arguments, selected, c_options) { + if !backend.compile(llvm_path, output_path, effective_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 352378c..a5ededa 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -161,10 +161,13 @@ Function :: struct { Global :: struct { name: symbol.Id, + link_name: string, type: types.Type, expr: Expr_Id, static_value: i64, is_static: bool, + external: bool, + writable: bool, dependencies: [dynamic]Global_Id, calls: []Function_Id, direct_problem: bool, @@ -210,6 +213,7 @@ destroy_module :: proc(module: ^Module) { delete(function.calls, module.allocator) } for global in module.globals { + delete(global.link_name, module.allocator) delete(global.dependencies) delete(global.calls, module.allocator) } diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index ecb1a60..db600c5 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -128,8 +128,11 @@ Function :: struct { Global :: struct { name: symbol.Id, + link_name: string, type: types.Type, is_static: bool, + external: bool, + writable: bool, static_value: i64, initializer: []Instruction, problematic: bool, @@ -171,6 +174,7 @@ destroy_module :: proc(module: ^Module) { destroy_instructions(function.instructions, module.allocator) } for global in module.globals { + delete(global.link_name, module.allocator) destroy_instructions(global.initializer, module.allocator) } for value in module.strings { diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 3d06bc4..7fbfa7b 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -600,7 +600,15 @@ emit_instruction_stream :: proc( emit_recovery_value(emitter, instruction_index, instruction, "invalid global reference type") continue } - if global.is_static { + if global.external { + fmt.sbprintf( + &emitter.builder, + " %%v%d = load %s, ptr @%s\n", + instruction_index, + llvm_type(global.type, &emitter.module.types), + global.link_name, + ) + } else if global.is_static { fmt.sbprintf( &emitter.builder, " %%v%d = load %s, ptr @bro.g.%d\n", @@ -632,6 +640,15 @@ emit_instruction_stream :: proc( emit_recovery_value(emitter, instruction_index, instruction, "invalid global address") continue } + global := emitter.module.globals[global_id] + if global.external { + fmt.sbprintf( + &emitter.builder, + " %%v%d = getelementptr %s, ptr @%s, i64 0\n", + instruction_index, llvm_type(instruction.type, &emitter.module.types), global.link_name, + ) + continue + } fmt.sbprintf( &emitter.builder, " %%v%d = getelementptr %s, ptr @bro.g.%d, i64 0\n", @@ -1369,7 +1386,28 @@ emit_instruction_stream :: proc( emit_globals :: proc(emitter: ^Emitter) { for global, global_id in emitter.module.globals { - if global.is_static { + if global.external { + if global.problematic { + continue + } + duplicate := false + for previous in emitter.module.globals[:global_id] { + if previous.external && !previous.problematic && previous.link_name == global.link_name { + duplicate = true + break + } + } + if duplicate { + continue + } + fmt.sbprintf( + &emitter.builder, + "@%s = external %s %s\n", + global.link_name, + "global" if global.writable else "constant", + llvm_type(global.type, &emitter.module.types), + ) + } else if global.is_static { fmt.sbprintf( &emitter.builder, "@bro.g.%d = internal constant %s ", @@ -1462,7 +1500,7 @@ emit_strings :: proc(emitter: ^Emitter) { emit_global_accessors :: proc(emitter: ^Emitter) { placeholder_function := ir.Function{result=types.I64} for global, global_id in emitter.module.globals { - if global.is_static { + if global.is_static || global.external { continue } type_name := llvm_type(global.type, &emitter.module.types) @@ -1500,7 +1538,7 @@ emit_global_accessors :: proc(emitter: ^Emitter) { emit_constructor :: proc(emitter: ^Emitter) { count := 0 for global in emitter.module.globals { - if !global.is_static && !global.problematic { + if !global.is_static && !global.external && !global.problematic { count += 1 } } @@ -1513,7 +1551,7 @@ emit_constructor :: proc(emitter: ^Emitter) { ) strings.write_string(&emitter.builder, "define internal void @bro.init() {\nentry:\n") for global, global_id in emitter.module.globals { - if !global.is_static && !global.problematic { + if !global.is_static && !global.external && !global.problematic { fmt.sbprintf(&emitter.builder, " %%g%d = call %s @bro.get.%d()\n", global_id, llvm_type(global.type, &emitter.module.types), global_id) } } diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index ed173be..7db40f7 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -9,6 +9,7 @@ import "../symbol" import "../target" import "../types" import "core:fmt" +import "core:math" import "core:mem" import "core:os" import "core:path/filepath" @@ -296,6 +297,625 @@ c_record_by_value_reason :: proc(result: ^cimport.Result, value: cimport.Type_Id return "" } +add_import_unsupported :: proc(state: ^State, pkg: ast.Package_Id, name: string, reason: string) { + if len(name) == 0 || len(reason) == 0 { + return + } + append(&state.module.unsupported, ast.Unsupported{ + pkg=pkg, + name=symbol.intern(state.symbols, name), + reason=strings.clone(reason, state.allocator), + }) +} + +find_trampoline :: proc(result: ^cimport.Result, symbol: string) -> (cimport.Trampoline, bool) { + for trampoline in result.trampolines { + if trampoline.symbol == symbol { + return trampoline, true + } + } + return {}, false +} + +add_c_trampoline :: proc(state: ^State, trampoline: cimport.Trampoline) { + if len(trampoline.symbol) == 0 || len(trampoline.source) == 0 { + return + } + for existing in state.module.c_trampolines { + if existing.symbol == trampoline.symbol { + return + } + } + append(&state.module.c_trampolines, ast.Trampoline{ + symbol=strings.clone(trampoline.symbol, state.allocator), + source=strings.clone(trampoline.source, state.allocator), + header=strings.clone(trampoline.header, state.allocator), + }) +} + +add_import_expr :: proc(state: ^State, expr: ast.Expr) -> ast.Expr_Id { + id := ast.expr_id(len(state.module.exprs)) + append(&state.module.exprs, expr) + return id +} + +add_macro_value_expr :: proc(state: ^State, value: cimport.Macro_Value, span: source.Span) -> ast.Expr_Id { + #partial switch value.kind { + case .Integer: + if value.negative { + magnitude := value.integer + operand := add_import_expr(state, ast.Expr{ + kind=.Integer, span=span, integer=magnitude, + left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return add_import_expr(state, ast.Expr{ + kind=.Negate, span=span, left=operand, right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + return add_import_expr(state, ast.Expr{ + kind=.Integer, span=span, integer=value.integer, + left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Float: + return add_import_expr(state, ast.Expr{ + kind=.Float, span=span, integer=value.integer, + left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case: + } + return add_import_expr(state, ast.Expr{ + kind=.Invalid, span=span, left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, + diagnostic=source.add(state.diagnostics, span, "unsupported C macro value"), + }) +} + +MAX_MACRO_ZERO_DEPTH :: 64 +MAX_MACRO_ZERO_NODES :: 65_536 + +find_macro_record_name :: proc( + state: ^State, + pkg: ast.Package_Id, + value: types.Type, +) -> (symbol.Id, bool) { + resolved := types.resolve_alias(value, &state.module.type_store) + for item, index in state.module.type_store.nodes { + if item.pkg != u32(pkg) || item.name == 0 { + continue + } + candidate := types.DYNAMIC_START+types.Type(index) + if types.resolve_alias(candidate, &state.module.type_store) == resolved { + return symbol.Id(item.name), true + } + } + return symbol.INVALID, false +} + +add_macro_zero_expr :: proc( + state: ^State, + pkg: ast.Package_Id, + value_type: types.Type, + span: source.Span, + depth: int, + remaining: ^int, +) -> (ast.Expr_Id, bool) { + if depth > MAX_MACRO_ZERO_DEPTH || remaining^ <= 0 { + return ast.INVALID_EXPR, false + } + remaining^ -= 1 + store := &state.module.type_store + resolved := types.resolve_alias(value_type, store) + #partial switch types.kind(resolved, store) { + case .Scalar: + kind := ast.Expr_Kind.Integer + if types.is_float(resolved, state.selected) { + kind = .Float + } + return add_import_expr(state, ast.Expr{ + kind=kind, span=span, integer=0, + left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }), true + case .Optional: + if !types.is_optional_pointer(resolved, store) { + return ast.INVALID_EXPR, false + } + return add_import_expr(state, ast.Expr{ + kind=.None, span=span, + left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }), true + case .Array: + item, ok := types.node(store, resolved) + if !ok || item.count > u64(max(int)) || item.count > u64(remaining^) { + return ast.INVALID_EXPR, false + } + args := make([]ast.Expr_Id, int(item.count), state.allocator) + for index in 0.. i128 { + magnitude := i128(value.integer) + return -magnitude if value.negative else magnitude +} + +convert_macro_integer :: proc( + value: i128, + destination: types.Type, + selected: target.Target, +) -> (Converted_Macro_Value, bool) { + bits := types.bits(destination, selected) + if bits <= 0 || bits > 64 { + return {}, false + } + if types.is_unsigned(destination, selected) { + modulus := i128(1) << u32(bits) + wrapped := value % modulus + if wrapped < 0 { + wrapped += modulus + } + return Converted_Macro_Value{ + kind=.Integer, + integer=u64(wrapped), + }, true + } + if !types.is_signed(destination, selected) { + return {}, false + } + limit := i128(1) << u32(bits-1) + if value < -limit || value >= limit { + return {}, false + } + if value < 0 { + return Converted_Macro_Value{ + kind=.Integer, + integer=u64(-value), + negative=true, + }, true + } + return Converted_Macro_Value{kind=.Integer, integer=u64(value)}, true +} + +macro_float_value :: proc( + value: cimport.Macro_Value, + source_type: types.Type, + selected: target.Target, +) -> (f64, bool) { + if value.kind != .Float || !types.is_float(source_type, selected) { + return 0, false + } + number := transmute(f64)value.integer + if types.bits(source_type, selected) == 32 { + number = f64(f32(number)) + } + if math.is_nan(number) || math.is_inf(number) { + return 0, false + } + return number, true +} + +convert_macro_float :: proc( + number: f64, + destination: types.Type, + selected: target.Target, +) -> (Converted_Macro_Value, bool) { + if !types.is_float(destination, selected) { + return {}, false + } + converted := number + if types.bits(destination, selected) == 32 { + rounded := f64(f32(number)) + if math.is_inf(rounded) { + return {}, false + } + converted = rounded + } + return Converted_Macro_Value{ + kind=.Float, + integer=transmute(u64)converted, + }, true +} + +convert_macro_field_value :: proc( + state: ^State, + result: ^cimport.Result, + value: cimport.Macro_Value, + field_type: types.Type, + pkg: ast.Package_Id, + record_mapping: []types.Type, + type_mapping: []types.Type, +) -> (Converted_Macro_Value, bool) { + store := &state.module.type_store + destination := types.resolve_alias(field_type, store) + source_type := translate_c_type( + state, result, value.type, pkg, record_mapping, type_mapping, + ) + if !types.is_concrete_scalar(source_type) { + return {}, false + } + if types.is_optional_pointer(destination, store) { + if types.is_concrete_integer(source_type) && + value.kind == .Integer && macro_integer_value(value) == 0 { + return Converted_Macro_Value{kind=.Null}, true + } + return {}, false + } + if types.is_concrete_integer(source_type) { + if value.kind != .Integer { + return {}, false + } + integer := macro_integer_value(value) + if types.is_concrete_integer(destination) { + return convert_macro_integer(integer, destination, state.selected) + } + if types.is_float(destination, state.selected) { + return convert_macro_float(f64(integer), destination, state.selected) + } + return {}, false + } + number, number_ok := macro_float_value(value, source_type, state.selected) + if !number_ok { + return {}, false + } + if types.is_float(destination, state.selected) { + return convert_macro_float(number, destination, state.selected) + } + if types.is_concrete_integer(destination) { + truncated := math.trunc(number) + bits := types.bits(destination, state.selected) + if bits <= 0 || bits > 64 { + return {}, false + } + if types.is_signed(destination, state.selected) { + limit := f64(i128(1) << u32(bits-1)) + if truncated < -limit || truncated >= limit { + return {}, false + } + } else if types.is_unsigned(destination, state.selected) { + limit := f64(i128(1) << u32(bits)) + if truncated < 0 || truncated >= limit { + return {}, false + } + } else { + return {}, false + } + return convert_macro_integer(i128(truncated), destination, state.selected) + } + return {}, false +} + +add_converted_macro_value_expr :: proc( + state: ^State, + value: Converted_Macro_Value, + span: source.Span, +) -> (ast.Expr_Id, bool) { + #partial switch value.kind { + case .Integer: + return add_macro_value_expr(state, cimport.Macro_Value{ + kind=.Integer, + integer=value.integer, + negative=value.negative, + }, span), true + case .Float: + return add_macro_value_expr(state, cimport.Macro_Value{ + kind=.Float, + integer=value.integer, + }, span), true + case .Null: + return add_import_expr(state, ast.Expr{ + kind=.None, span=span, + left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }), true + case: + } + return ast.INVALID_EXPR, false +} + +add_macro_aggregate_expr :: proc( + state: ^State, + result: ^cimport.Result, + macro: cimport.Macro_Constant, + record_type: types.Type, + pkg: ast.Package_Id, + record_mapping: []types.Type, + type_mapping: []types.Type, + span: source.Span, +) -> (ast.Expr_Id, bool) { + fields := types.fields_for(&state.module.type_store, record_type) + union_record := types.is_union(record_type, &state.module.type_store) + initializer_count := 1 if union_record else len(fields) + if len(fields) == 0 || len(macro.values) > initializer_count { + return ast.INVALID_EXPR, false + } + converted := make([]Converted_Macro_Value, len(macro.values), state.allocator) + defer delete(converted, state.allocator) + for value, index in macro.values { + converted[index], _ = convert_macro_field_value( + state, + result, + value, + fields[index].type, + pkg, + record_mapping, + type_mapping, + ) + if converted[index].kind == .Invalid { + return ast.INVALID_EXPR, false + } + } + args := make([]ast.Expr_Id, initializer_count, state.allocator) + remaining := MAX_MACRO_ZERO_NODES + for index in 0.. (ast.Global_Id, bool) { + for global, index in module.globals { + if global.pkg == pkg && global.name == name { + return ast.global_id(index), true + } + } + return ast.INVALID_GLOBAL, false +} + +find_function_in_package :: proc(module: ^ast.Module, pkg: ast.Package_Id, name: symbol.Id) -> bool { + for function in module.functions { + if function.pkg == pkg && function.name == name { + return true + } + } + return false +} + +remove_value_declarations_in_package :: proc( + state: ^State, + pkg: ast.Package_Id, + name: symbol.Id, +) { + function_index := 0 + for function_index < len(state.module.functions) { + function := state.module.functions[function_index] + if function.pkg != pkg || function.name != name { + function_index += 1 + continue + } + delete(function.params, state.allocator) + delete(function.body, state.allocator) + delete(function.link_name, state.allocator) + delete(function.unsupported_reason, state.allocator) + ordered_remove(&state.module.functions, function_index) + } + global_index := 0 + for global_index < len(state.module.globals) { + global := state.module.globals[global_index] + if global.pkg != pkg || global.name != name { + global_index += 1 + continue + } + delete(global.link_name, state.allocator) + ordered_remove(&state.module.globals, global_index) + } + unsupported_index := 0 + for unsupported_index < len(state.module.unsupported) { + item := state.module.unsupported[unsupported_index] + if item.pkg != pkg || item.name != name { + unsupported_index += 1 + continue + } + delete(item.reason, state.allocator) + ordered_remove(&state.module.unsupported, unsupported_index) + } +} + +add_external_variable_global :: proc( + state: ^State, + result: ^cimport.Result, + pkg: ast.Package_Id, + variable: cimport.Variable, + record_mapping: []types.Type, + type_mapping: []types.Type, + span: source.Span, +) { + name := symbol.intern(state.symbols, variable.name) + variable_type := translate_c_type(state, result, variable.type, pkg, record_mapping, type_mapping) + unsupported_reason := variable.reason + if len(unsupported_reason) == 0 { + unsupported_reason = c_record_by_value_reason(result, variable.type) + } + if len(unsupported_reason) == 0 && !types.is_runtime_value(variable_type, &state.module.type_store) { + unsupported_reason = "C variable type is not supported" + } + if len(unsupported_reason) > 0 { + add_import_unsupported(state, pkg, variable.name, unsupported_reason) + return + } + if find_function_in_package(state.module, pkg, name) { + add_import_unsupported(state, pkg, variable.name, "C variable conflicts with a function declaration") + return + } + if existing, ok := find_global_in_package(state.module, pkg, name); ok { + global := state.module.globals[existing] + if !global.external || !types.equal(global.type, variable_type) || global.writable != variable.mutable { + add_import_unsupported(state, pkg, variable.name, "conflicting C declarations for variable") + } + return + } + _ = ast.global_id(len(state.module.globals)) + append(&state.module.globals, ast.Global{ + span=span, + name=name, + link_name=strings.clone(variable.name, state.allocator), + pkg=pkg, + file=ast.INVALID_FILE, + type=variable_type, + immutable=true, + external=true, + writable=variable.mutable, + expr=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + +add_macro_constant_global :: proc( + state: ^State, + result: ^cimport.Result, + pkg: ast.Package_Id, + macro: cimport.Macro_Constant, + record_mapping: []types.Type, + type_mapping: []types.Type, + span: source.Span, +) { + name := symbol.intern(state.symbols, macro.name) + remove_value_declarations_in_package(state, pkg, name) + if macro.aggregate { + type_name := symbol.intern(state.symbols, macro.type_name) + named := types.find_named(&state.module.type_store, u32(pkg), u32(type_name)) + record_type := types.resolve_alias(named, &state.module.type_store) + if !types.is_record(record_type, &state.module.type_store) || + types.is_opaque_struct(record_type, &state.module.type_store) { + add_import_unsupported(state, pkg, macro.name, "C macro aggregate type is not supported") + return + } + expr, ok := add_macro_aggregate_expr( + state, + result, + macro, + record_type, + pkg, + record_mapping, + type_mapping, + span, + ) + if !ok { + add_import_unsupported(state, pkg, macro.name, "C macro aggregate initializer is not representable") + return + } + _ = ast.global_id(len(state.module.globals)) + append(&state.module.globals, ast.Global{ + span=span, + name=name, + pkg=pkg, + file=ast.INVALID_FILE, + type=record_type, + immutable=true, + expr=expr, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return + } + macro_type := translate_c_type(state, result, macro.type, pkg, record_mapping, type_mapping) + if !types.is_runtime_value(macro_type, &state.module.type_store) { + add_import_unsupported(state, pkg, macro.name, "C macro constant type is not supported") + return + } + expr := add_macro_value_expr(state, macro.value, span) + _ = ast.global_id(len(state.module.globals)) + append(&state.module.globals, ast.Global{ + span=span, + name=name, + pkg=pkg, + file=ast.INVALID_FILE, + type=macro_type, + immutable=true, + expr=expr, + diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + load_header :: proc(state: ^State, path: string, import_span: source.Span) -> ast.Package_Id { canonical, ok := filepath.abs(path, state.allocator) if !ok { @@ -460,14 +1080,38 @@ load_header :: proc(state: ^State, path: string, import_span: source.Span) -> as variadic=function.variadic, params=params, result=function_result, + link_name=strings.clone(function.link_name, state.allocator), unsupported_reason=strings.clone(unsupported_reason, state.allocator), diagnostic=source.INVALID_DIAGNOSTIC, }) + // Emit the wrapper only for a `static inline` that survives as supported. + // cimport may translate its signature fine yet the by-value record layout + // checks above can still reject it; a wrapper for an uncallable function + // would just be dead external code. + if len(unsupported_reason) == 0 && len(function.link_name) > 0 { + if trampoline, ok := find_trampoline(&result, function.link_name); ok { + add_c_trampoline(state, trampoline) + } + } + } + for variable in result.variables { + add_external_variable_global( + state, &result, pkg_id, variable, record_mapping, type_mapping, import_span, + ) + } + for macro in result.macros { + add_macro_constant_global( + state, &result, pkg_id, macro, record_mapping, type_mapping, import_span, + ) } for item in result.unsupported { + name := symbol.intern(state.symbols, item.name) + if item.final_macro { + remove_value_declarations_in_package(state, pkg_id, name) + } append(&state.module.unsupported, ast.Unsupported{ pkg=pkg_id, - name=symbol.intern(state.symbols, item.name), + name=name, reason=strings.clone(item.reason, state.allocator), }) } diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 2b24cf9..9e96447 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -644,10 +644,13 @@ lower :: proc(hir_module: ^hir.Module, allocator := context.allocator) -> ir.Mod _ = ir.global_id(len(module.globals)) append(&module.globals, ir.Global{ name=global.name, + link_name=fmt.aprintf("%s", global.link_name, allocator=allocator), type=global.type, is_static=global.is_static, + external=global.external, + writable=global.writable, static_value=global.static_value, - initializer=nil if global.is_static else lower_global_initializer(hir_module, global, allocator), + initializer=nil if global.is_static || global.external else lower_global_initializer(hir_module, global, allocator), problematic=global.problematic, diagnostic=global.diagnostic, }) diff --git a/compiler_tests.odin b/compiler_tests.odin index 90bfb68..b46161e 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -992,6 +992,31 @@ main :: func() void { testing.expect(t, sentinel_error) } +@(test) +immutable_pointer_and_slice_bindings_preserve_mutable_pointees :: proc(t: ^testing.T) { + text := `main :: func() void { + values [2]mut u8 = [1, 2] + pointer *mut u8 :: (&values).ptr + slice []mut u8 :: values[0..] + pointer[0] = 3 + slice[1] = 4 +} +` + 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) + + testing.expect_value(t, len(diagnostics.items), 0) +} + @(test) c_variadic_calls_promote_extras_and_emit_variadic_llvm :: proc(t: ^testing.T) { text := `variadic :: c_func(tag c_int, ...) c_int @@ -2118,6 +2143,14 @@ Fake_Cimport_State :: struct { saw_options: bool, } +Conflict_Cimport_State :: struct { + calls: int, +} + +Symbol_Conflict_Cimport_State :: struct { + calls: int, +} + fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result { state := (^Fake_Cimport_State)(user_data) state.calls += 1 @@ -2142,10 +2175,143 @@ fake_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, alloca variadic=true, reason=fmt.aprintf("", allocator=allocator), }) + append(&result.variables, cimport.Variable{ + name=fmt.aprintf("fake_global", allocator=allocator), + type=cimport.Type_Id(0), + mutable=true, + reason=fmt.aprintf("", allocator=allocator), + }) + append(&result.macros, cimport.Macro_Constant{ + name=fmt.aprintf("FAKE_MAGIC", allocator=allocator), + type=cimport.Type_Id(0), + value={kind=.Integer, type=cimport.Type_Id(0), integer=7}, + reason=fmt.aprintf("", allocator=allocator), + }) result.available = true return result } +conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result { + state := (^Conflict_Cimport_State)(user_data) + state.calls += 1 + result := cimport.init_result(allocator) + kind := cimport.Type_Kind.C_Int + if strings.has_suffix(request.path, "second.h") { + kind = .C_Long + } + append(&result.types, cimport.Type{kind=kind, child=cimport.INVALID_TYPE}) + append(&result.variables, cimport.Variable{ + name=fmt.aprintf("conflict_global", allocator=allocator), + type=cimport.Type_Id(0), + mutable=true, + reason=fmt.aprintf("", allocator=allocator), + }) + result.available = true + return result +} + +symbol_conflict_cimport_backend :: proc(user_data: rawptr, request: cimport.Request, allocator: mem.Allocator) -> cimport.Result { + state := (^Symbol_Conflict_Cimport_State)(user_data) + state.calls += 1 + result := cimport.init_result(allocator) + append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE}) + if strings.has_suffix(request.path, "variable.h") { + append(&result.variables, cimport.Variable{ + name=fmt.aprintf("conflict_symbol", allocator=allocator), + type=cimport.Type_Id(0), + mutable=true, + reason=fmt.aprintf("", allocator=allocator), + }) + } else { + append(&result.functions, cimport.Function{ + name=fmt.aprintf("conflict_symbol", allocator=allocator), + result=cimport.Type_Id(0), + reason=fmt.aprintf("", allocator=allocator), + }) + } + result.available = true + return result +} + +main_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result { + result := cimport.init_result(allocator) + append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE}) + append(&result.variables, cimport.Variable{ + name=fmt.aprintf("main", allocator=allocator), + type=cimport.Type_Id(0), + mutable=true, + reason=fmt.aprintf("", allocator=allocator), + }) + result.available = true + return result +} + +write_conflict_cimport_backend :: proc(_: rawptr, _: cimport.Request, allocator: mem.Allocator) -> cimport.Result { + result := cimport.init_result(allocator) + append(&result.types, cimport.Type{kind=.C_Int, child=cimport.INVALID_TYPE}) + append(&result.variables, cimport.Variable{ + name=fmt.aprintf("write", allocator=allocator), + type=cimport.Type_Id(0), + mutable=true, + reason=fmt.aprintf("", allocator=allocator), + }) + result.available = true + return result +} + +count_substring_occurrences :: proc(text, needle: string) -> int { + if len(needle) == 0 { + return 0 + } + count := 0 + for index := 0; index + len(needle) <= len(text); index += 1 { + if text[index:index + len(needle)] == needle { + count += 1 + } + } + return count +} + +find_cimport_variable :: proc(result: ^cimport.Result, name: string) -> (^cimport.Variable, bool) { + for &variable in result.variables { + if variable.name == name { + return &variable, true + } + } + return nil, false +} + +count_cimport_variables :: proc(result: ^cimport.Result, name: string) -> int { + count := 0 + for variable in result.variables { + if variable.name == name { + count += 1 + } + } + return count +} + +find_cimport_macro :: proc(result: ^cimport.Result, name: string) -> (^cimport.Macro_Constant, bool) { + for ¯o in result.macros { + if macro.name == name { + return ¯o, true + } + } + return nil, false +} + +cimport_has_named_result :: proc(result: ^cimport.Result, name: string) -> bool { + if _, ok := find_cimport_macro(result, name); ok { + return true + } + for item in result.unsupported { + if item.name == name { + return true + } + } + return false +} + @(test) cimport_backend_is_replaceable :: proc(t: ^testing.T) { state := Fake_Cimport_State{available=true} @@ -2156,10 +2322,238 @@ cimport_backend_is_replaceable :: proc(t: ^testing.T) { testing.expect(t, result.available) testing.expect_value(t, state.calls, 1) testing.expect_value(t, len(result.functions), 1) + testing.expect_value(t, len(result.variables), 1) + testing.expect_value(t, len(result.macros), 1) testing.expect_value(t, result.functions[0].name, "fake_value") + testing.expect_value(t, result.variables[0].name, "fake_global") + testing.expect_value(t, result.macros[0].name, "FAKE_MAGIC") testing.expect(t, result.functions[0].variadic) } +@(test) +libclang_import_preserves_external_object_and_final_macro_semantics :: proc(t: ^testing.T) { + options := cimport.Options{ + include_paths=[]string{"examples/interop/header/include"}, + defines=[]string{"BROLANG_FEATURE"}, + } + result := cimport.import_header( + options, + "examples/interop/header/include/native.h", + target.DEFAULT, + ) + defer cimport.destroy_result(&result) + + testing.expect(t, result.available) + testing.expect_value(t, result.error_message, "") + + tls, found_tls := find_cimport_variable(&result, "imported_tls_global") + testing.expect(t, found_tls) + if found_tls { + testing.expect(t, strings.contains(tls.reason, "thread-local C variables are not supported")) + } + + const_array, found_const_array := find_cimport_variable(&result, "imported_const_array") + testing.expect(t, found_const_array) + if found_const_array { + testing.expect(t, !const_array.mutable) + testing.expect(t, const_array.type != cimport.INVALID_TYPE) + if const_array.type != cimport.INVALID_TYPE { + array_type := result.types[const_array.type] + testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array) + testing.expect(t, array_type.child != cimport.INVALID_TYPE) + if array_type.child != cimport.INVALID_TYPE { + testing.expect_value(t, result.types[array_type.child].kind, cimport.Type_Kind.C_Int) + } + } + } + + typedef_const_array, found_typedef_const_array := find_cimport_variable( + &result, "imported_typedef_const_array", + ) + testing.expect(t, found_typedef_const_array) + if found_typedef_const_array { + testing.expect(t, !typedef_const_array.mutable) + testing.expect_value(t, typedef_const_array.reason, "") + testing.expect(t, typedef_const_array.type != cimport.INVALID_TYPE) + if typedef_const_array.type != cimport.INVALID_TYPE { + array_type := result.types[typedef_const_array.type] + testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array) + testing.expect(t, !array_type.mutable) + testing.expect_value(t, array_type.count, u64(2)) + } + } + + redeclared_array, found_redeclared_array := find_cimport_variable( + &result, "imported_redeclared_array", + ) + testing.expect(t, found_redeclared_array) + testing.expect_value(t, count_cimport_variables(&result, "imported_redeclared_array"), 1) + if found_redeclared_array { + testing.expect_value(t, redeclared_array.reason, "") + testing.expect(t, redeclared_array.type != cimport.INVALID_TYPE) + if redeclared_array.type != cimport.INVALID_TYPE { + array_type := result.types[redeclared_array.type] + testing.expect_value(t, array_type.kind, cimport.Type_Kind.Array) + testing.expect_value(t, array_type.count, u64(4)) + } + } + + repeated, found_repeated := find_cimport_macro(&result, "IMPORTED_REPEAT") + testing.expect(t, found_repeated) + if found_repeated { + testing.expect_value(t, repeated.value.integer, u64(123)) + } + testing.expect(t, !cimport_has_named_result(&result, "IMPORTED_GUARDED")) + testing.expect(t, !cimport_has_named_result(&result, "IMPORTED_ONCE")) + + negative_decimal, found_negative_decimal := find_cimport_macro(&result, "IMPORTED_NEG_DECIMAL") + testing.expect(t, found_negative_decimal) + if found_negative_decimal { + testing.expect_value(t, result.types[negative_decimal.type].kind, cimport.Type_Kind.C_Long) + testing.expect_value(t, negative_decimal.value.integer, u64(2147483648)) + testing.expect(t, negative_decimal.value.negative) + } + + negative_hex, found_negative_hex := find_cimport_macro(&result, "IMPORTED_NEG_HEX") + testing.expect(t, found_negative_hex) + if found_negative_hex { + testing.expect_value(t, result.types[negative_hex.type].kind, cimport.Type_Kind.C_Uint) + testing.expect_value(t, negative_hex.value.integer, u64(0x80000000)) + testing.expect(t, !negative_hex.value.negative) + } + + negative_uint, found_negative_uint := find_cimport_macro(&result, "IMPORTED_NEG_UINT") + testing.expect(t, found_negative_uint) + if found_negative_uint { + testing.expect_value(t, result.types[negative_uint.type].kind, cimport.Type_Kind.C_Uint) + testing.expect_value(t, negative_uint.value.integer, u64(0xffffffff)) + testing.expect(t, !negative_uint.value.negative) + } + + conversions, found_conversions := find_cimport_macro(&result, "IMPORTED_CONVERSIONS") + testing.expect(t, found_conversions) + if found_conversions { + testing.expect_value(t, len(conversions.values), 5) + expected_kinds := [?]cimport.Type_Kind{ + .C_Int, + .C_Double, + .C_Int, + .C_Float, + .C_Int, + } + for value, index in conversions.values { + testing.expect(t, value.type != cimport.INVALID_TYPE) + if value.type != cimport.INVALID_TYPE { + testing.expect_value(t, result.types[value.type].kind, expected_kinds[index]) + } + } + } +} + +@(test) +final_macros_override_same_named_c_value_declarations :: 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) + c_options := cimport.Options{ + include_paths=[]string{"examples/interop/header/include"}, + defines=[]string{"BROLANG_FEATURE"}, + } + module, loaded := loader.load( + "examples/interop/header/app", + &sources, + &diagnostics, + &symbols, + c_options=c_options, + ) + defer ast.destroy_module(&module) + testing.expect(t, loaded) + + hir_module := checker.check(&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, "@IMPORTED_SHADOW_OBJECT = external")) + testing.expect(t, !strings.contains(llvm_text, "declare i32 @IMPORTED_SHADOW_FUNCTION(")) + testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_UNSUPPORTED = external")) + testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_EMPTY_SHADOW = external")) +} + +@(test) +static_inline_c_functions_route_through_generated_trampolines :: 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) + c_options := cimport.Options{ + include_paths=[]string{"examples/interop/header/include"}, + defines=[]string{"BROLANG_FEATURE"}, + } + module, loaded := loader.load( + "examples/interop/header/app", + &sources, + &diagnostics, + &symbols, + c_options=c_options, + ) + defer ast.destroy_module(&module) + testing.expect(t, loaded) + + // Symbols are namespaced by a header-path hash, so match by suffix. + scalar_symbol := "" + record_symbol := "" + for trampoline in module.c_trampolines { + testing.expect(t, strings.has_prefix(trampoline.symbol, "__brolang_inline_")) + if strings.has_suffix(trampoline.symbol, "_imported_inline") { + scalar_symbol = trampoline.symbol + } + if strings.has_suffix(trampoline.symbol, "_imported_inline_record") { + record_symbol = trampoline.symbol + } + // The variadic static inline is unsupported and must not be wrapped. + testing.expect(t, !strings.has_suffix(trampoline.symbol, "_imported_inline_variadic")) + } + testing.expect(t, scalar_symbol != "") + testing.expect(t, record_symbol != "") + + // A static inline whose signature translates but is rejected by the loader's + // by-value layout checks keeps its cimport-assigned link_name yet must not + // emit a wrapper — it is uncallable, so the wrapper would be dead code. + bad_layout_link := "" + for function in module.functions { + if symbol.resolve(&symbols, function.name) == "imported_inline_bad_layout" { + testing.expect(t, len(function.unsupported_reason) > 0) + bad_layout_link = function.link_name + } + } + testing.expect(t, bad_layout_link != "") // cimport did generate a wrapper symbol + for trampoline in module.c_trampolines { + testing.expect(t, trampoline.symbol != bad_layout_link) + } + + hir_module := checker.check(&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, fmt.tprintf("@%s(", scalar_symbol))) + testing.expect(t, strings.contains(llvm_text, fmt.tprintf("@%s(", record_symbol))) + // The internal-linkage C symbol itself is never declared or called directly. + testing.expect(t, !strings.contains(llvm_text, "@imported_inline(")) +} + @(test) loader_injects_and_caches_cimport_backend_per_compilation :: proc(t: ^testing.T) { sources := source.init_store() @@ -2195,6 +2589,336 @@ loader_injects_and_caches_cimport_backend_per_compilation :: proc(t: ^testing.T) found_variadic = found_variadic || function.variadic } testing.expect(t, found_variadic) + found_external := false + found_macro := false + for global in module.globals { + name := symbol.resolve(&symbols, global.name) + found_external = found_external || (name == "fake_global" && global.external && global.writable) + found_macro = found_macro || name == "FAKE_MAGIC" + } + testing.expect(t, found_external) + testing.expect(t, found_macro) +} + +@(test) +conflicting_external_c_globals_are_diagnosed_and_deduped :: 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 := Conflict_Cimport_State{} + options := cimport.Options{backend={import_header=conflict_cimport_backend, user_data=&state}} + module, loaded := loader.load( + "examples/interop/header_conflict", + &sources, + &diagnostics, + &symbols, + c_options=options, + ) + defer ast.destroy_module(&module) + testing.expect(t, loaded) + testing.expect_value(t, state.calls, 2) + + hir_module := checker.check(&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_conflict := false + for diagnostic in diagnostics.items { + if strings.contains(diagnostic.message, "conflicting external C variable declarations for 'conflict_global'") { + found_conflict = true + } + } + testing.expect(t, found_conflict) + testing.expect_value(t, count_substring_occurrences(llvm_text, "@conflict_global = external global"), 1) + testing.expect(t, strings.contains(llvm_text, "@conflict_global = external global i32")) + testing.expect(t, !strings.contains(llvm_text, "@conflict_global = external global i64")) +} + +@(test) +external_c_global_and_function_link_name_conflict_is_diagnosed :: 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 := Symbol_Conflict_Cimport_State{} + options := cimport.Options{backend={import_header=symbol_conflict_cimport_backend, user_data=&state}} + module, loaded := loader.load( + "examples/interop/header_symbol_conflict", + &sources, + &diagnostics, + &symbols, + c_options=options, + ) + defer ast.destroy_module(&module) + testing.expect(t, loaded) + testing.expect_value(t, state.calls, 2) + + hir_module := checker.check(&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_conflict := false + for diagnostic in diagnostics.items { + if strings.contains(diagnostic.message, "external C variable 'conflict_symbol' conflicts with a C function declaration") { + found_conflict = true + } + } + testing.expect(t, found_conflict) + testing.expect(t, !strings.contains(llvm_text, "@conflict_symbol = external global")) + testing.expect(t, strings.contains(llvm_text, "declare i32 @conflict_symbol()")) + testing.expect(t, !strings.contains(llvm_text, "load i32, ptr @conflict_symbol")) +} + +@(test) +external_c_global_named_main_is_omitted_for_root_entry_point :: 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) + options := cimport.Options{backend={import_header=main_conflict_cimport_backend}} + module, loaded := loader.load( + "examples/interop/header_main_conflict", + &sources, + &diagnostics, + &symbols, + c_options=options, + ) + defer ast.destroy_module(&module) + testing.expect(t, loaded) + + hir_module := checker.check(&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_conflict := false + for diagnostic in diagnostics.items { + found_conflict = + found_conflict || + strings.contains( + diagnostic.message, + "external C variable 'main' conflicts with the program entry point", + ) + } + testing.expect(t, found_conflict) + testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main("), 1) + testing.expect(t, !strings.contains(llvm_text, "@main = external")) +} + +@(test) +external_c_global_named_main_is_omitted_for_synthesized_entry_point :: 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) + options := cimport.Options{backend={import_header=main_conflict_cimport_backend}} + module, loaded := loader.load( + "examples/interop/header_main_conflict_missing", + &sources, + &diagnostics, + &symbols, + c_options=options, + ) + defer ast.destroy_module(&module) + testing.expect(t, loaded) + + hir_module := checker.check(&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_conflict := false + found_missing_main := false + for diagnostic in diagnostics.items { + found_conflict = + found_conflict || + strings.contains( + diagnostic.message, + "external C variable 'main' conflicts with the program entry point", + ) + found_missing_main = + found_missing_main || + strings.contains(diagnostic.message, "missing or unusable main function") + } + testing.expect(t, found_conflict) + testing.expect(t, found_missing_main) + testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main("), 1) + testing.expect(t, !strings.contains(llvm_text, "@main = external")) +} + +@(test) +external_c_global_named_write_is_omitted_for_compiler_runtime :: 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) + options := cimport.Options{backend={import_header=write_conflict_cimport_backend}} + module, loaded := loader.load( + "examples/interop/header_write_conflict", + &sources, + &diagnostics, + &symbols, + c_options=options, + ) + defer ast.destroy_module(&module) + testing.expect(t, loaded) + + hir_module := checker.check(&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_conflict := false + for diagnostic in diagnostics.items { + found_conflict = + found_conflict || + strings.contains( + diagnostic.message, + "external C variable 'write' conflicts with the compiler runtime", + ) + } + testing.expect(t, found_conflict) + testing.expect_value(t, count_substring_occurrences(llvm_text, "declare i64 @write("), 1) + testing.expect(t, !strings.contains(llvm_text, "@write = external")) + testing.expect(t, strings.contains(llvm_text, "call void @bro.trap")) +} + +@(test) +tls_reference_and_const_external_array_assignment_are_diagnosed :: 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) + c_options := cimport.Options{include_paths=[]string{"examples/interop/header/include"}} + module, loaded := loader.load( + "examples/interop/header_unsupported", + &sources, + &diagnostics, + &symbols, + c_options=c_options, + ) + defer ast.destroy_module(&module) + testing.expect(t, loaded) + + hir_module := checker.check(&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_tls := false + found_excess_aggregate := false + found_signed_narrow := false + found_shadowed_unsupported := false + found_float_overflow := false + found_empty_shadow := false + found_inline_variadic := false + not_writable_count := 0 + for diagnostic in diagnostics.items { + found_tls = + found_tls || + strings.contains( + diagnostic.message, + "C declaration 'imported_tls_global' is unavailable: thread-local C variables are not supported", + ) + found_excess_aggregate = + found_excess_aggregate || + strings.contains( + diagnostic.message, + "C declaration 'IMPORTED_TOO_MANY_COLOR' is unavailable: C macro aggregate initializer is not representable", + ) + found_signed_narrow = + found_signed_narrow || + strings.contains( + diagnostic.message, + "C declaration 'IMPORTED_SIGNED_NARROW_BAD' is unavailable: C macro aggregate initializer is not representable", + ) + found_shadowed_unsupported = + found_shadowed_unsupported || + strings.contains( + diagnostic.message, + "C declaration 'IMPORTED_SHADOW_UNSUPPORTED' is unavailable: C macro is not a supported constant", + ) + found_float_overflow = + found_float_overflow || + strings.contains( + diagnostic.message, + "C declaration 'IMPORTED_FLOAT_OVERFLOW' is unavailable: C macro is not a supported constant", + ) + found_empty_shadow = + found_empty_shadow || + strings.contains( + diagnostic.message, + "C declaration 'IMPORTED_EMPTY_SHADOW' is unavailable: C macro has no replacement value", + ) + found_inline_variadic = + found_inline_variadic || + strings.contains( + diagnostic.message, + "C declaration 'imported_inline_variadic' is unavailable: variadic static inline C functions are not supported", + ) + if strings.contains(diagnostic.message, "assignment target is not writable") { + not_writable_count += 1 + } + } + testing.expect(t, found_tls) + testing.expect(t, found_excess_aggregate) + testing.expect(t, found_signed_narrow) + testing.expect(t, found_shadowed_unsupported) + testing.expect(t, found_float_overflow) + testing.expect(t, found_empty_shadow) + testing.expect(t, found_inline_variadic) + testing.expect(t, not_writable_count >= 5) + testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_EMPTY_SHADOW = external")) + testing.expect( + t, + strings.contains( + llvm_text, + "@imported_typedef_const_array = external constant [2 x i32]", + ), + ) + testing.expect_value( + t, + count_substring_occurrences( + llvm_text, + "@imported_redeclared_array = external global [4 x i32]", + ), + 1, + ) + testing.expect( + t, + !strings.contains( + llvm_text, + "ptr @imported_const_array_record, i64 0", + ), + ) + testing.expect(t, !strings.contains(llvm_text, "@IMPORTED_SHADOW_UNSUPPORTED = external")) } @(test) diff --git a/examples/interop/header/app/main.bro b/examples/interop/header/app/main.bro index f4397a1..586607f 100644 --- a/examples/interop/header/app/main.bro +++ b/examples/interop/header/app/main.bro @@ -21,6 +21,37 @@ main :: func() void { _ = native.imported_apply(double_value, 21) _ = call_mapper(double_value) _ = native.configured_value(9) + _ = native.IMPORTED_SHADOW_OBJECT + _ = native.IMPORTED_SHADOW_FUNCTION + native.imported_global = native.IMPORTED_MAGIC + native.imported_record_global.value = native.IMPORTED_MAGIC + color native.Imported_Color :: native.IMPORTED_COLOR + _ = native.imported_check_state( + color, + native.IMPORTED_MAGIC, + native.imported_const_global, + native.imported_global, + native.imported_record_global.value, + native.IMPORTED_OCTAL, + native.IMPORTED_UINT, + native.IMPORTED_REDEFINED, + native.IMPORTED_ACTIVE_AFTER_INACTIVE_UNDEF, + native.IMPORTED_REPEAT, + native.IMPORTED_HEX_E, + native.IMPORTED_NEG_DECIMAL, + native.IMPORTED_NEG_HEX, + native.IMPORTED_NEG_UINT, + native.IMPORTED_FLOAT, + native.IMPORTED_DOUBLE, + ) + partial native.Imported_Color :: native.IMPORTED_PARTIAL_COLOR + nested native.Imported_Zero_Outer :: native.IMPORTED_ZERO_NESTED + first_union native.Imported_Zero_Union :: native.IMPORTED_FIRST_UNION + _ = native.imported_check_zero_state(partial, nested, first_union) + native.imported_mutable_array_record.values[0] = 42 + _ = native.imported_check_array_record() + conversions native.Imported_Conversions :: native.IMPORTED_CONVERSIONS + _ = native.imported_check_conversions(conversions) signed i8 :: -2 unsigned u16 :: 3 float_value f32 :: 4.0 @@ -28,4 +59,7 @@ main :: func() void { pointer *u8 :: "ok".ptr nullable ?*u8 :: pointer _ = native.imported_variadic(7, signed, unsigned, float_value, c_float_value, pointer, nullable) + inline_scalar c_int :: native.imported_inline(5) + inline_record native.Imported_Value :: native.imported_inline_record(native.Imported_Value { value = 7 }) + _ = native.imported_check_inline(inline_scalar, inline_record.value) } diff --git a/examples/interop/header/include/child.h b/examples/interop/header/include/child.h index a762f95..66bc52e 100644 --- a/examples/interop/header/include/child.h +++ b/examples/interop/header/include/child.h @@ -2,5 +2,6 @@ #define BROLANG_CHILD_H int child_value(int value); +extern int child_shared_global; #endif diff --git a/examples/interop/header/include/guarded.h b/examples/interop/header/include/guarded.h new file mode 100644 index 0000000..b89d8bc --- /dev/null +++ b/examples/interop/header/include/guarded.h @@ -0,0 +1,6 @@ +#ifndef BROLANG_GUARDED_H +#define BROLANG_GUARDED_H + +#define IMPORTED_GUARDED 321 + +#endif diff --git a/examples/interop/header/include/native.h b/examples/interop/header/include/native.h index 97fdc34..5b80b93 100644 --- a/examples/interop/header/include/native.h +++ b/examples/interop/header/include/native.h @@ -2,6 +2,15 @@ #define BROLANG_NATIVE_H #include +#include "repeat.h" +#undef IMPORTED_REPEAT +#include "repeat.h" +#include "guarded.h" +#undef IMPORTED_GUARDED +#include "guarded.h" +#include "once.h" +#undef IMPORTED_ONCE +#include "once.h" typedef int imported_int; typedef imported_int imported_int_alias; @@ -11,9 +20,43 @@ typedef int (*Imported_Mapper)(int value); typedef struct Imported_Value { int value; } Imported_Value; +typedef struct Imported_Color { + unsigned char r; + unsigned char g; + unsigned char b; + unsigned char a; +} Imported_Color; +typedef struct Imported_Zero_Inner { + float weight; + int values[2]; + const char *label; +} Imported_Zero_Inner; +typedef union Imported_Zero_Union { + int integer; + double decimal; +} Imported_Zero_Union; +typedef struct Imported_Zero_Outer { + int head; + Imported_Zero_Inner inner; + Imported_Zero_Union choice; +} Imported_Zero_Outer; +typedef struct Imported_Array_Record { + int values[2]; +} Imported_Array_Record; +typedef struct Imported_Conversions { + unsigned char wrapped; + int truncated; + float from_integer; + double from_float; + const char *pointer; +} Imported_Conversions; +typedef struct Imported_Signed_Narrow { + signed char value; +} Imported_Signed_Narrow; typedef union Imported_Union { int value; } Imported_Union; +typedef int Imported_Const_Array[2]; typedef struct Imported_Bitfield { unsigned value : 1; } Imported_Bitfield; @@ -47,14 +90,103 @@ int imported_read(const Imported_Handle *handle); int imported_string(const char *value); int imported_apply(Imported_Mapper mapper, int value); Imported_Value imported_by_value(Imported_Value value); +int imported_check_state( + Imported_Color color, + int macro_value, + int const_value, + int global_value, + int record_value, + int octal_value, + unsigned int uint_value, + int redefined_value, + int inactive_undef_value, + int repeated_value, + unsigned int hex_e_value, + long negative_decimal_value, + unsigned int negative_hex_value, + unsigned int negative_uint_value, + float float_value, + double double_value +); +int imported_check_zero_state( + Imported_Color partial, + Imported_Zero_Outer nested, + Imported_Zero_Union first_union +); +int imported_check_array_record(void); +int imported_check_conversions(Imported_Conversions value); int imported_volatile(volatile int *value); _Bool imported_bool(_Bool value); extern int imported_global; +extern const int imported_const_global; +extern const int imported_const_array[2]; +extern const Imported_Const_Array imported_typedef_const_array; +extern int imported_redeclared_array[]; +extern int imported_redeclared_array[4]; +extern Imported_Value imported_record_global; +extern const Imported_Array_Record imported_const_array_record; +extern Imported_Array_Record imported_mutable_array_record; +extern _Thread_local int imported_tls_global; +extern int IMPORTED_SHADOW_OBJECT; +int IMPORTED_SHADOW_FUNCTION(void); +extern int IMPORTED_SHADOW_UNSUPPORTED; +extern int IMPORTED_EMPTY_SHADOW; + +#define CLITERAL(type) (type) +#define IMPORTED_SHADOW_OBJECT 71 +#define IMPORTED_SHADOW_FUNCTION 72 +#define IMPORTED_SHADOW_UNSUPPORTED (1 + 2) +#define IMPORTED_MAGIC 42 +#define IMPORTED_COLOR CLITERAL(Imported_Color){ 255, 255, 255, 255 } +#define IMPORTED_PARTIAL_COLOR CLITERAL(Imported_Color){ 7 } +#define IMPORTED_ZERO_NESTED CLITERAL(Imported_Zero_Outer){ 5 } +#define IMPORTED_FIRST_UNION CLITERAL(Imported_Zero_Union){ 9 } +#define IMPORTED_CONVERSIONS CLITERAL(Imported_Conversions){ -1, 3.75, 42, 16777217.0f, 0 } +#define IMPORTED_SIGNED_NARROW_BAD CLITERAL(Imported_Signed_Narrow){ 128 } +#define IMPORTED_TOO_MANY_COLOR CLITERAL(Imported_Color){ 1, 2, 3, 4, 5 } +#define IMPORTED_BAD_EXPR (1 + 2) +#define IMPORTED_OCTAL 010 +#define IMPORTED_UINT 4294967295U +#define IMPORTED_HEX_E 0xDEADBEEF +#define IMPORTED_NEG_DECIMAL -2147483648 +#define IMPORTED_NEG_HEX -0x80000000 +#define IMPORTED_NEG_UINT -1U +#define IMPORTED_FLOAT 2.5f +#define IMPORTED_DOUBLE 6.25 +#define IMPORTED_FLOAT_OVERFLOW 1e40f +#define IMPORTED_EMPTY_SHADOW +#define IMPORTED_REDEFINED 1 +#undef IMPORTED_REDEFINED +#define IMPORTED_REDEFINED 2 +#define IMPORTED_GONE 3 +#undef IMPORTED_GONE +#define IMPORTED_REDEFINED_BAD 1 +#undef IMPORTED_REDEFINED_BAD +#define IMPORTED_REDEFINED_BAD (1 + 2) +#define IMPORTED_ACTIVE_AFTER_INACTIVE_UNDEF 77 +#if 0 +#undef IMPORTED_ACTIVE_AFTER_INACTIVE_UNDEF +#endif static inline int imported_inline(int value) { - return value; + return value + 1; } +static inline Imported_Value imported_inline_record(Imported_Value value) { + Imported_Value result = { value.value * 2 }; + return result; +} + +static inline int imported_inline_bad_layout(Imported_Packed value) { + return value.first; +} + +static inline int imported_inline_variadic(int marker, ...) { + return marker; +} + +int imported_check_inline(int scalar, int record); + #ifdef BROLANG_FEATURE int configured_value(int value); #endif diff --git a/examples/interop/header/include/once.h b/examples/interop/header/include/once.h new file mode 100644 index 0000000..59df0d5 --- /dev/null +++ b/examples/interop/header/include/once.h @@ -0,0 +1,3 @@ +#pragma once + +#define IMPORTED_ONCE 456 diff --git a/examples/interop/header/include/repeat.h b/examples/interop/header/include/repeat.h new file mode 100644 index 0000000..d8aa4b6 --- /dev/null +++ b/examples/interop/header/include/repeat.h @@ -0,0 +1 @@ +#define IMPORTED_REPEAT 123 diff --git a/examples/interop/header/native.c b/examples/interop/header/native.c index ba7d5c3..39169a7 100644 --- a/examples/interop/header/native.c +++ b/examples/interop/header/native.c @@ -8,6 +8,15 @@ struct Imported_Handle { }; static struct Imported_Handle handle = {7}; +int imported_global = 0; +const int imported_const_global = 9; +const int imported_const_array[2] = {10, 20}; +const Imported_Const_Array imported_typedef_const_array = {11, 12}; +int imported_redeclared_array[4] = {13, 14, 15, 16}; +Imported_Value imported_record_global = {40}; +const Imported_Array_Record imported_const_array_record = {{20, 21}}; +Imported_Array_Record imported_mutable_array_record = {{0, 1}}; +int child_shared_global = 0; int child_value(int value) { return value; @@ -47,6 +56,97 @@ int imported_apply(Imported_Mapper mapper, int value) { return result; } +int imported_check_state( + Imported_Color color, + int macro_value, + int const_value, + int global_value, + int record_value, + int octal_value, + unsigned int uint_value, + int redefined_value, + int inactive_undef_value, + int repeated_value, + unsigned int hex_e_value, + long negative_decimal_value, + unsigned int negative_hex_value, + unsigned int negative_uint_value, + float float_value, + double double_value +) { + if (!(color.r == 255 && + color.g == 255 && + color.b == 255 && + color.a == 255 && + macro_value == 42 && + const_value == 9 && + global_value == 42 && + record_value == 42 && + octal_value == 8 && + uint_value == 4294967295U && + redefined_value == 2 && + inactive_undef_value == 77 && + repeated_value == 123 && + hex_e_value == 0xDEADBEEF && + negative_decimal_value == -2147483648L && + negative_hex_value == 0x80000000U && + negative_uint_value == 0xFFFFFFFFU && + float_value == 2.5f && + double_value == 6.25)) { + abort(); + } + return 0; +} + +int imported_check_zero_state( + Imported_Color partial, + Imported_Zero_Outer nested, + Imported_Zero_Union first_union +) { + if (!(partial.r == 7 && + partial.g == 0 && + partial.b == 0 && + partial.a == 0 && + nested.head == 5 && + nested.inner.weight == 0.0f && + nested.inner.values[0] == 0 && + nested.inner.values[1] == 0 && + nested.inner.label == NULL && + nested.choice.integer == 0 && + first_union.integer == 9)) { + abort(); + } + return 0; +} + +int imported_check_array_record(void) { + if (!(imported_const_array_record.values[0] == 20 && + imported_const_array_record.values[1] == 21 && + imported_mutable_array_record.values[0] == 42 && + imported_mutable_array_record.values[1] == 1)) { + abort(); + } + return 0; +} + +int imported_check_conversions(Imported_Conversions value) { + if (!(value.wrapped == 255 && + value.truncated == 3 && + value.from_integer == 42.0f && + value.from_float == 16777216.0 && + value.pointer == NULL)) { + abort(); + } + return 0; +} + +int imported_check_inline(int scalar, int record) { + if (!(scalar == 6 && record == 14)) { + abort(); + } + return 0; +} + int imported_variadic(int marker, ...) { va_list args; va_start(args, marker); diff --git a/examples/interop/header_conflict/first.h b/examples/interop/header_conflict/first.h new file mode 100644 index 0000000..1afeb4b --- /dev/null +++ b/examples/interop/header_conflict/first.h @@ -0,0 +1 @@ +/* Imported through a fake cimport backend in compiler tests. */ diff --git a/examples/interop/header_conflict/main.bro b/examples/interop/header_conflict/main.bro new file mode 100644 index 0000000..9ed7a15 --- /dev/null +++ b/examples/interop/header_conflict/main.bro @@ -0,0 +1,7 @@ +first :: import "first.h" +second :: import "second.h" + +main :: func() void { + _ = first.conflict_global + _ = second.conflict_global +} diff --git a/examples/interop/header_conflict/second.h b/examples/interop/header_conflict/second.h new file mode 100644 index 0000000..1afeb4b --- /dev/null +++ b/examples/interop/header_conflict/second.h @@ -0,0 +1 @@ +/* Imported through a fake cimport backend in compiler tests. */ diff --git a/examples/interop/header_duplicate/main.bro b/examples/interop/header_duplicate/main.bro index 1913a8a..ef821e8 100644 --- a/examples/interop/header_duplicate/main.bro +++ b/examples/interop/header_duplicate/main.bro @@ -4,4 +4,7 @@ child :: import "../header/include/child.h" main :: func() void { _ = native.child_value(1) _ = child.child_value(2) + native.child_shared_global = 7 + child.child_shared_global = native.child_shared_global + _ = native.child_shared_global + child.child_shared_global } diff --git a/examples/interop/header_main_conflict/main.bro b/examples/interop/header_main_conflict/main.bro new file mode 100644 index 0000000..4aff593 --- /dev/null +++ b/examples/interop/header_main_conflict/main.bro @@ -0,0 +1,5 @@ +native :: import "native.h" + +main :: func() void { + _ = native.main +} diff --git a/examples/interop/header_main_conflict/native.h b/examples/interop/header_main_conflict/native.h new file mode 100644 index 0000000..1afeb4b --- /dev/null +++ b/examples/interop/header_main_conflict/native.h @@ -0,0 +1 @@ +/* Imported through a fake cimport backend in compiler tests. */ diff --git a/examples/interop/header_main_conflict_missing/main.bro b/examples/interop/header_main_conflict_missing/main.bro new file mode 100644 index 0000000..0aa0768 --- /dev/null +++ b/examples/interop/header_main_conflict_missing/main.bro @@ -0,0 +1,3 @@ +native :: import "native.h" + +value :: native.main diff --git a/examples/interop/header_main_conflict_missing/native.h b/examples/interop/header_main_conflict_missing/native.h new file mode 100644 index 0000000..1afeb4b --- /dev/null +++ b/examples/interop/header_main_conflict_missing/native.h @@ -0,0 +1 @@ +/* Imported through a fake cimport backend in compiler tests. */ diff --git a/examples/interop/header_symbol_conflict/function.h b/examples/interop/header_symbol_conflict/function.h new file mode 100644 index 0000000..1afeb4b --- /dev/null +++ b/examples/interop/header_symbol_conflict/function.h @@ -0,0 +1 @@ +/* Imported through a fake cimport backend in compiler tests. */ diff --git a/examples/interop/header_symbol_conflict/main.bro b/examples/interop/header_symbol_conflict/main.bro new file mode 100644 index 0000000..4b6ec75 --- /dev/null +++ b/examples/interop/header_symbol_conflict/main.bro @@ -0,0 +1,7 @@ +variable :: import "variable.h" +function :: import "function.h" + +main :: func() void { + _ = variable.conflict_symbol + _ = function.conflict_symbol() +} diff --git a/examples/interop/header_symbol_conflict/variable.h b/examples/interop/header_symbol_conflict/variable.h new file mode 100644 index 0000000..1afeb4b --- /dev/null +++ b/examples/interop/header_symbol_conflict/variable.h @@ -0,0 +1 @@ +/* Imported through a fake cimport backend in compiler tests. */ diff --git a/examples/interop/header_unsupported/main.bro b/examples/interop/header_unsupported/main.bro index 1311461..99f9b72 100644 --- a/examples/interop/header_unsupported/main.bro +++ b/examples/interop/header_unsupported/main.bro @@ -6,7 +6,14 @@ use_enum :: c_func(value native.Imported_Enum) void use_opaque :: c_func(value native.Imported_Handle) void main :: func() void { - _ = native.IMPORTED_MACRO + _ = native.IMPORTED_BAD_EXPR + _ = native.IMPORTED_REDEFINED_BAD + _ = native.IMPORTED_GONE + _ = native.IMPORTED_TOO_MANY_COLOR + _ = native.IMPORTED_SIGNED_NARROW_BAD + _ = native.IMPORTED_SHADOW_UNSUPPORTED + _ = native.IMPORTED_FLOAT_OVERFLOW + _ = native.IMPORTED_EMPTY_SHADOW _ = native.imported_by_value() _ = native.imported_bitfield() _ = native.imported_packed() @@ -16,6 +23,11 @@ main :: func() void { _ = native.imported_anonymous() _ = native.imported_volatile() _ = native.imported_bool() - _ = native.imported_global - _ = native.imported_inline(1) + native.imported_const_global = 1 + native.imported_const_array = [1, 2] + native.imported_typedef_const_array = [1, 2] + native.imported_typedef_const_array[0] = 1 + native.imported_const_array_record.values[0] = 42 + _ = native.imported_tls_global + _ = native.imported_inline_variadic(1) } diff --git a/examples/interop/header_write_conflict/main.bro b/examples/interop/header_write_conflict/main.bro new file mode 100644 index 0000000..2636368 --- /dev/null +++ b/examples/interop/header_write_conflict/main.bro @@ -0,0 +1,5 @@ +native :: import "native.h" + +main :: func() void { + _ = native.write +} diff --git a/examples/interop/header_write_conflict/native.h b/examples/interop/header_write_conflict/native.h new file mode 100644 index 0000000..1afeb4b --- /dev/null +++ b/examples/interop/header_write_conflict/native.h @@ -0,0 +1 @@ +/* Imported through a fake cimport backend in compiler tests. */