translate-c file emission

This commit is contained in:
2026-06-24 22:53:20 +02:00
parent f6fc25a899
commit 4cb0ad7f25
7 changed files with 512 additions and 4 deletions
+19 -4
View File
@@ -151,11 +151,26 @@
- array rvalues (e.g. a by-value array return) are materialized into a - array rvalues (e.g. a by-value array return) are materialized into a
temporary before slicing, matching the for-loop iterable lowering temporary before slicing, matching the for-loop iterable lowering
11. c header imports and automatic native brolang bindings (implemented)
- `brolang translate-c <header.h> [--target ...] [--c-include-path ...] [--c-define ...]`
prints native `.bro` bindings for a C header to stdout (the offline counterpart of the
in-memory `native :: import "x.h"`); reuses the libclang `cimport.Result`
- emitter lives in `compiler/translatec`; `render_type` mirrors `loader.translate_c_type`
one-to-one so emitted source re-parses to identical types (guarded by a round-trip test)
- added a native type-alias declaration `Name :: alias T` (parser/lexer/token surface; the
`types.define_alias` / `.Alias` machinery already existed) so C typedefs and callback
typedefs round-trip
- emits functions, complete/opaque structs (collapsing `typedef struct {...} Foo`),
typedef aliases, and scalar/aggregate/enum-member constants
- C unions, external variables, static-inline functions, and unsupported declarations have
no hand-writable spelling and are emitted as `# unsupported in bindings:` comments
(functions that reference an un-spellable union therefore keep a dangling reference)
## A word on multi-unwrap ## A word on multi-unwrap
Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated. Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated.
```honey ```
name: ?[]u8 = get_name() name: ?[]u8 = get_name()
age: ?u8 = get_age() age: ?u8 = get_age()
if name and age |n, a| { if name and age |n, a| {
@@ -166,7 +181,7 @@ if name and age |n, a| {
**With guard clause on multiple values:** **With guard clause on multiple values:**
```honey ```
if name and hat |n, h : n == "Huginn" and h.brand == .gucci| { if name and hat |n, h : n == "Huginn" and h.brand == .gucci| {
print("{s}'s got that drip\n", {n}) print("{s}'s got that drip\n", {n})
} }
@@ -174,7 +189,7 @@ if name and hat |n, h : n == "Huginn" and h.brand == .gucci| {
Parentheses around the expression are optional, but can aid readability when combined with guards: Parentheses around the expression are optional, but can aid readability when combined with guards:
```honey ```
# without parentheses # without parentheses
if name and hat |n, h : guard| { ... } if name and hat |n, h : guard| { ... }
@@ -186,7 +201,7 @@ if (name and hat) |n, h : guard| { ... }
The `and` in multi-unwrap short-circuits left-to-right: The `and` in multi-unwrap short-circuits left-to-right:
```honey ```
if get_name() and get_hat() |n, h| { if get_name() and get_hat() |n, h| {
# get_hat() is only called if get_name() returned non-none # get_hat() is only called if get_name() returned non-none
} }
+1
View File
@@ -20,6 +20,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "c_struct": return .Keyword_C_Struct case "c_struct": return .Keyword_C_Struct
case "enum": return .Keyword_Enum case "enum": return .Keyword_Enum
case "distinct": return .Keyword_Distinct case "distinct": return .Keyword_Distinct
case "alias": return .Keyword_Alias
case "import": return .Keyword_Import case "import": return .Keyword_Import
case "return": return .Keyword_Return case "return": return .Keyword_Return
case "mut": return .Keyword_Mut case "mut": return .Keyword_Mut
+17
View File
@@ -1505,6 +1505,19 @@ parse_distinct :: proc(parser: ^Parser, name: token.Token) {
_ = finish_statement(parser) _ = finish_statement(parser)
} }
parse_alias :: proc(parser: ^Parser, name: token.Token) {
start := advance(parser)
child := parse_type(parser)
id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol))
if !types.define_alias(&parser.module.type_store, id, child) {
source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name))
}
if !types.is_valid(child) {
source.add(parser.diagnostics, start.span, "alias declarations require a backing type")
}
_ = finish_statement(parser)
}
parse_enum :: proc(parser: ^Parser, name: token.Token) { parse_enum :: proc(parser: ^Parser, name: token.Token) {
start := advance(parser) start := advance(parser)
explicit_backing := false explicit_backing := false
@@ -1736,6 +1749,10 @@ parse_top_level :: proc(parser: ^Parser) {
parse_distinct(parser, name) parse_distinct(parser, name)
return return
} }
if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_Alias {
parse_alias(parser, name)
return
}
expr := parse_expression(parser) expr := parse_expression(parser)
_ = ast.global_id(len(parser.module.globals)) _ = ast.global_id(len(parser.module.globals))
+1
View File
@@ -54,6 +54,7 @@ Kind :: enum u8 {
Keyword_C_Struct, Keyword_C_Struct,
Keyword_Enum, Keyword_Enum,
Keyword_Distinct, Keyword_Distinct,
Keyword_Alias,
Keyword_Import, Keyword_Import,
Keyword_Return, Keyword_Return,
Keyword_Mut, Keyword_Mut,
+334
View File
@@ -0,0 +1,334 @@
package translatec
// translatec renders a parsed C header (cimport.Result) as native brolang (.bro)
// source the offline counterpart to the in-memory `native :: import "x.h"`
// path. The type spellings here MUST mirror loader.translate_c_type so the
// emitted source re-parses to the same types the in-memory import produces; the
// round-trip parser test (compiler_tests.odin) guards that invariant.
//
// Constructs with no hand-writable brolang spelling C unions, external
// variables, static inline functions, and otherwise unsupported declarations
// are emitted as `# unsupported in bindings:` comments rather than dropped, so
// the output is an honest record of the whole header.
import "../cimport"
import "core:fmt"
import "core:mem"
import "core:strings"
emit :: proc(result: ^cimport.Result, header: string, allocator := context.allocator) -> string {
b := strings.builder_make(allocator)
fmt.sbprintf(&b, "# generated by brolang translate-c from %s\n\n", header)
record_names := record_name_table(result, allocator)
defer delete(record_names, allocator)
emit_records(&b, result, record_names)
emit_aliases(&b, result, record_names)
emit_macros(&b, result, record_names)
emit_functions(&b, result, record_names)
emit_variables(&b, result)
emit_unsupported(&b, result)
return strings.to_string(b)
}
// record_name_table maps each record index to the brolang identifier it is
// emitted under: its C tag name, or for an anonymous record named only by a
// typedef that typedef's name (the typedef is then skipped). Truly anonymous
// records fall back to the loader's synthetic `__c_record_N`.
record_name_table :: proc(result: ^cimport.Result, allocator: mem.Allocator) -> []string {
names := make([]string, len(result.records), allocator)
for record, idx in result.records {
if len(record.name) > 0 {
names[idx] = record.name
} else {
names[idx] = fmt.aprintf("__c_record_%d", idx, allocator=allocator)
}
}
for alias in result.aliases {
if len(alias.reason) > 0 {
continue
}
ti := alias.type
if int(ti) < 0 || int(ti) >= len(result.types) {
continue
}
target := result.types[ti]
if target.kind != .Record || int(target.record) >= len(result.records) {
continue
}
if len(result.records[target.record].name) == 0 {
names[target.record] = alias.name
}
}
return names
}
// render_type writes the brolang spelling of a C type. Mirror of
// loader.translate_c_type keep the two in lockstep.
render_type :: proc(b: ^strings.Builder, result: ^cimport.Result, id: cimport.Type_Id, record_names: []string) {
if id == cimport.INVALID_TYPE || int(id) < 0 || int(id) >= len(result.types) {
strings.write_string(b, "void")
return
}
item := result.types[id]
switch item.kind {
case .Invalid: strings.write_string(b, "void")
case .Void: strings.write_string(b, "void")
case .C_Char: strings.write_string(b, "c_char")
case .C_Schar: strings.write_string(b, "c_schar")
case .C_Uchar: strings.write_string(b, "c_uchar")
case .C_Short: strings.write_string(b, "c_short")
case .C_Ushort: strings.write_string(b, "c_ushort")
case .C_Int: strings.write_string(b, "c_int")
case .C_Uint: strings.write_string(b, "c_uint")
case .C_Long: strings.write_string(b, "c_long")
case .C_Ulong: strings.write_string(b, "c_ulong")
case .C_Longlong: strings.write_string(b, "c_longlong")
case .C_Ulonglong: strings.write_string(b, "c_ulonglong")
case .C_Float: strings.write_string(b, "c_float")
case .C_Double: strings.write_string(b, "c_double")
case .C_Longdouble: strings.write_string(b, "c_longdouble")
case .Pointer:
// loader: optional(pointer(child, mutable, many=true)). A Function child
// yields the `?*c_func(...) T` callback spelling for free.
strings.write_string(b, "?*")
if item.mutable {
strings.write_string(b, "mut ")
}
render_type(b, result, item.child, record_names)
case .Array:
fmt.sbprintf(b, "[%d]", item.count)
render_type(b, result, item.child, record_names)
case .Function:
render_c_func(b, result, item.params, item.child, item.variadic, record_names)
case .Record:
if int(item.record) >= 0 && int(item.record) < len(record_names) {
strings.write_string(b, record_names[item.record])
} else {
strings.write_string(b, "void")
}
}
}
// render_c_func writes `c_func(arg0 T0, ...) R`. Params are named arg0.. because
// the parser requires parameter names; names do not affect type identity.
render_c_func :: proc(
b: ^strings.Builder,
result: ^cimport.Result,
params: []cimport.Type_Id,
ret: cimport.Type_Id,
variadic: bool,
record_names: []string,
) {
strings.write_string(b, "c_func(")
for param, index in params {
if index > 0 {
strings.write_string(b, ", ")
}
fmt.sbprintf(b, "arg%d ", index)
render_type(b, result, param, record_names)
}
if variadic {
if len(params) > 0 {
strings.write_string(b, ", ")
}
strings.write_string(b, "...")
}
strings.write_string(b, ") ")
render_type(b, result, ret, record_names)
}
emit_records :: proc(b: ^strings.Builder, result: ^cimport.Result, record_names: []string) {
wrote := false
for record, idx in result.records {
name := record_names[idx]
if record.kind == .Union {
fmt.sbprintf(b, "# unsupported in bindings: C union '%s' has no native spelling\n", name)
wrote = true
continue
}
if !record.complete || len(record.reason) > 0 {
// opaque / pointer-only struct
fmt.sbprintf(b, "%s :: c_struct\n", name)
wrote = true
continue
}
fmt.sbprintf(b, "%s :: c_struct {{\n", name)
for field in record.fields {
fmt.sbprintf(b, "\t%s ", field.name)
render_type(b, result, field.type, record_names)
strings.write_byte(b, '\n')
}
strings.write_string(b, "}\n")
wrote = true
}
if wrote {
strings.write_byte(b, '\n')
}
}
emit_aliases :: proc(b: ^strings.Builder, result: ^cimport.Result, record_names: []string) {
wrote := false
for alias in result.aliases {
if len(alias.reason) > 0 {
fmt.sbprintf(b, "# unsupported in bindings: typedef '%s' — %s\n", alias.name, alias.reason)
wrote = true
continue
}
// Skip a typedef that merely (re)names a record under the name we already
// emitted the record with (anonymous-struct collapse, or `typedef struct
// Foo Foo;`).
if ti := alias.type; int(ti) >= 0 && int(ti) < len(result.types) {
target := result.types[ti]
if target.kind == .Record && int(target.record) < len(record_names) &&
record_names[target.record] == alias.name {
continue
}
}
fmt.sbprintf(b, "%s :: alias ", alias.name)
render_type(b, result, alias.type, record_names)
strings.write_byte(b, '\n')
wrote = true
}
if wrote {
strings.write_byte(b, '\n')
}
}
emit_macros :: proc(b: ^strings.Builder, result: ^cimport.Result, record_names: []string) {
wrote := false
for macro in result.macros {
if len(macro.reason) > 0 {
fmt.sbprintf(b, "# unsupported in bindings: macro '%s' — %s\n", macro.name, macro.reason)
wrote = true
continue
}
if macro.aggregate {
emit_aggregate_macro(b, result, macro, record_names)
wrote = true
continue
}
strings.write_string(b, macro.name)
if macro_scalar_kind(result, macro.type) {
strings.write_byte(b, ' ')
render_type(b, result, macro.type, record_names)
}
strings.write_string(b, " :: ")
render_macro_value(b, macro.value)
strings.write_byte(b, '\n')
wrote = true
}
if wrote {
strings.write_byte(b, '\n')
}
}
emit_aggregate_macro :: proc(
b: ^strings.Builder,
result: ^cimport.Result,
macro: cimport.Macro_Constant,
record_names: []string,
) {
ti := macro.type
if int(ti) >= 0 && int(ti) < len(result.types) && result.types[ti].kind == .Record {
ridx := int(result.types[ti].record)
if ridx < len(result.records) {
record := result.records[ridx]
if len(record.fields) == len(macro.values) && !record_has_pointer_field(result, record) {
fmt.sbprintf(b, "%s :: %s {{", macro.name, record_names[ridx])
for field, index in record.fields {
if index > 0 {
strings.write_byte(b, ',')
}
fmt.sbprintf(b, " %s = ", field.name)
render_macro_value(b, macro.values[index])
}
strings.write_string(b, " }\n")
return
}
}
}
fmt.sbprintf(b, "# unsupported in bindings: aggregate macro '%s' has no native spelling\n", macro.name)
}
emit_functions :: proc(b: ^strings.Builder, result: ^cimport.Result, record_names: []string) {
wrote := false
for function in result.functions {
if len(function.reason) > 0 {
fmt.sbprintf(b, "# unsupported in bindings: function '%s' — %s\n", function.name, function.reason)
wrote = true
continue
}
if len(function.link_name) > 0 && function.link_name != function.name {
fmt.sbprintf(b, "# unsupported in bindings: function '%s' is a static inline function (needs trampoline)\n", function.name)
wrote = true
continue
}
fmt.sbprintf(b, "%s :: ", function.name)
render_c_func(b, result, function.params, function.result, function.variadic, record_names)
strings.write_byte(b, '\n')
wrote = true
}
if wrote {
strings.write_byte(b, '\n')
}
}
emit_variables :: proc(b: ^strings.Builder, result: ^cimport.Result) {
for variable in result.variables {
fmt.sbprintf(b, "# unsupported in bindings: external variable '%s' has no native spelling\n", variable.name)
}
}
emit_unsupported :: proc(b: ^strings.Builder, result: ^cimport.Result) {
for item in result.unsupported {
fmt.sbprintf(b, "# unsupported in bindings: %s — %s\n", item.name, item.reason)
}
}
render_macro_value :: proc(b: ^strings.Builder, value: cimport.Macro_Value) {
switch value.kind {
case .Integer:
if value.negative {
fmt.sbprintf(b, "-%d", value.integer)
} else {
fmt.sbprintf(b, "%d", value.integer)
}
case .Float:
number := transmute(f64)value.integer
text := fmt.tprintf("%v", number)
strings.write_string(b, text)
// Ensure it lexes as a float literal, not an integer.
if strings.index_byte(text, '.') < 0 &&
strings.index_byte(text, 'e') < 0 &&
strings.index_byte(text, 'E') < 0 {
strings.write_string(b, ".0")
}
case .Invalid:
strings.write_string(b, "0")
}
}
macro_scalar_kind :: proc(result: ^cimport.Result, id: cimport.Type_Id) -> bool {
if int(id) < 0 || int(id) >= len(result.types) {
return false
}
#partial switch result.types[id].kind {
case .C_Char, .C_Schar, .C_Uchar, .C_Short, .C_Ushort, .C_Int, .C_Uint,
.C_Long, .C_Ulong, .C_Longlong, .C_Ulonglong, .C_Float, .C_Double, .C_Longdouble:
return true
}
return false
}
record_has_pointer_field :: proc(result: ^cimport.Result, record: cimport.Record) -> bool {
for field in record.fields {
if int(field.type) >= 0 && int(field.type) < len(result.types) &&
result.types[field.type].kind == .Pointer {
return true
}
}
return false
}
+81
View File
@@ -17,6 +17,7 @@ import "./compiler/source"
import "./compiler/symbol" import "./compiler/symbol"
import "./compiler/target" import "./compiler/target"
import "./compiler/token" import "./compiler/token"
import "./compiler/translatec"
import "./compiler/types" import "./compiler/types"
import "core:fmt" import "core:fmt"
import "core:mem" import "core:mem"
@@ -5769,3 +5770,83 @@ native_enums_compile_and_run_across_packages :: proc(t: ^testing.T) {
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
} }
@(test)
translate_c_emits_native_bindings_and_round_trips :: proc(t: ^testing.T) {
result := cimport.init_result(context.allocator)
// type table: [0]=c_int, [1]=c_ulong, [2]=Record(Pair),
// [3]=Function(c_int)->c_int, [4]=Pointer->Function (callback)
append(&result.types, cimport.Type{kind = .C_Int, child = cimport.INVALID_TYPE})
append(&result.types, cimport.Type{kind = .C_Ulong, child = cimport.INVALID_TYPE})
append(&result.types, cimport.Type{kind = .Record, record = 0, child = cimport.INVALID_TYPE})
func_params := []cimport.Type_Id{cimport.Type_Id(0)}
append(&result.types, cimport.Type{kind = .Function, params = func_params, child = cimport.Type_Id(0)})
append(&result.types, cimport.Type{kind = .Pointer, child = cimport.Type_Id(3)})
// record 0: Pair { left c_int; right c_int }
pair_fields: [dynamic]cimport.Field
append(&pair_fields, cimport.Field{name = "left", type = cimport.Type_Id(0)})
append(&pair_fields, cimport.Field{name = "right", type = cimport.Type_Id(0)})
append(&result.records, cimport.Record{name = "Pair", fields = pair_fields, kind = .Struct, complete = true})
// record 1: union Choice -> commented (no native spelling)
choice_fields: [dynamic]cimport.Field
append(&choice_fields, cimport.Field{name = "tag", type = cimport.Type_Id(0)})
append(&result.records, cimport.Record{name = "Choice", fields = choice_fields, kind = .Union, complete = true})
// typedef aliases: a scalar and a callback function pointer
append(&result.aliases, cimport.Alias{name = "Size", type = cimport.Type_Id(1)})
append(&result.aliases, cimport.Alias{name = "Mapper", type = cimport.Type_Id(4)})
add_params := []cimport.Type_Id{cimport.Type_Id(0), cimport.Type_Id(0)}
append(&result.functions, cimport.Function{name = "imported_add", params = add_params, result = cimport.Type_Id(0)})
append(&result.macros, cimport.Macro_Constant{
name = "MAX_LEN",
type = cimport.Type_Id(0),
value = {kind = .Integer, type = cimport.Type_Id(0), integer = 256},
})
// external variable -> commented (no native spelling)
append(&result.variables, cimport.Variable{name = "some_global", type = cimport.Type_Id(0), mutable = true})
result.available = true
output := translatec.emit(&result, "test.h")
defer delete(output)
defer {
delete(result.types)
delete(result.records)
delete(result.aliases)
delete(result.functions)
delete(result.variables)
delete(result.macros)
delete(pair_fields)
delete(choice_fields)
}
testing.expect(t, strings.contains(output, "Pair :: c_struct {"))
testing.expect(t, strings.contains(output, "\tleft c_int"))
testing.expect(t, strings.contains(output, "\tright c_int"))
testing.expect(t, strings.contains(output, "Size :: alias c_ulong"))
testing.expect(t, strings.contains(output, "Mapper :: alias ?*c_func(arg0 c_int) c_int"))
testing.expect(t, strings.contains(output, "imported_add :: c_func(arg0 c_int, arg1 c_int) c_int"))
testing.expect(t, strings.contains(output, "MAX_LEN c_int :: 256"))
testing.expect(t, strings.contains(output, "# unsupported in bindings: C union 'Choice'"))
testing.expect(t, strings.contains(output, "# unsupported in bindings: external variable 'some_global'"))
// Round-trip: the emitted source must lex + parse with zero diagnostics.
// This guards render_type against drift from loader.translate_c_type and
// exercises the new `alias` declaration syntax.
source_file := source.Source{path = "bindings.bro", text = output}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
}
+59
View File
@@ -4,6 +4,7 @@ import "./compiler"
import "./compiler/cimport" import "./compiler/cimport"
import "./compiler/linker" import "./compiler/linker"
import "./compiler/target" import "./compiler/target"
import "./compiler/translatec"
import "core:fmt" import "core:fmt"
import "core:os/os2" import "core:os/os2"
@@ -85,9 +86,67 @@ print_usage :: proc() {
fmt.eprintln( fmt.eprintln(
"usage: brolang <package-directory> -o <executable> [--target aarch64-macos] [--c-link <path> | --c-library-path <dir> | --c-library <name> | --c-include-path <dir> | --c-define <name[=value]>]...", "usage: brolang <package-directory> -o <executable> [--target aarch64-macos] [--c-link <path> | --c-library-path <dir> | --c-library <name> | --c-include-path <dir> | --c-define <name[=value]>]...",
) )
fmt.eprintln(
" brolang translate-c <header.h> [--target aarch64-macos] [--c-include-path <dir> | --c-define <name[=value]>]...",
)
}
// run_translate_c emits native brolang bindings for a C header to stdout, the
// offline counterpart of `native :: import "x.h"`.
run_translate_c :: proc(args: []string) -> int {
if len(args) < 3 {
print_usage()
return 2
}
header := args[2]
selected := target.DEFAULT
include_paths: [dynamic]string
defines: [dynamic]string
defer delete(include_paths)
defer delete(defines)
cursor := 3
for cursor < len(args) {
option := args[cursor]
cursor += 1
if cursor >= len(args) {
fmt.eprintfln("missing value for %s", option)
return 2
}
value := args[cursor]
cursor += 1
switch option {
case "--c-include-path":
append(&include_paths, value)
case "--c-define":
append(&defines, value)
case "--target":
parsed, ok := target.parse(value)
if !ok {
fmt.eprintfln("unknown target '%s'", value)
return 2
}
selected = parsed
case:
fmt.eprintfln("unknown option '%s'", option)
return 2
}
}
c_options := cimport.Options{include_paths=include_paths[:], defines=defines[:]}
result := cimport.import_header(c_options, header, selected)
defer cimport.destroy_result(&result)
if !result.available {
message := result.error_message if len(result.error_message) > 0 else "failed to import header"
fmt.eprintln(message)
return 1
}
fmt.print(translatec.emit(&result, header))
return 0
} }
main :: proc() { main :: proc() {
if len(os2.args) >= 2 && os2.args[1] == "translate-c" {
os2.exit(run_translate_c(os2.args))
}
options, valid := parse_cli_args(os2.args) options, valid := parse_cli_args(os2.args)
if !valid { if !valid {
print_usage() print_usage()