From 4cb0ad7f2536b60e6a06c78ea48adfd644610d8f Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Wed, 24 Jun 2026 22:53:20 +0200 Subject: [PATCH] translate-c file emission --- TODO.md | 23 +- compiler/lexer/lexer.odin | 1 + compiler/parser/parser.odin | 17 ++ compiler/token/token.odin | 1 + compiler/translatec/translatec.odin | 334 ++++++++++++++++++++++++++++ compiler_tests.odin | 81 +++++++ main.odin | 59 +++++ 7 files changed, 512 insertions(+), 4 deletions(-) create mode 100644 compiler/translatec/translatec.odin diff --git a/TODO.md b/TODO.md index 656e67b..32441a3 100644 --- a/TODO.md +++ b/TODO.md @@ -151,11 +151,26 @@ - array rvalues (e.g. a by-value array return) are materialized into a temporary before slicing, matching the for-loop iterable lowering +11. c header imports and automatic native brolang bindings (implemented) + - `brolang translate-c [--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 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| { @@ -166,7 +181,7 @@ if name and age |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}) } @@ -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: -```honey +``` # without parentheses 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: -```honey +``` if get_name() and get_hat() |n, h| { # get_hat() is only called if get_name() returned non-none } diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 27c700a..c365d90 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -20,6 +20,7 @@ keyword_kind :: proc(text: string) -> token.Kind { case "c_struct": return .Keyword_C_Struct case "enum": return .Keyword_Enum case "distinct": return .Keyword_Distinct + case "alias": return .Keyword_Alias case "import": return .Keyword_Import case "return": return .Keyword_Return case "mut": return .Keyword_Mut diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 919c192..1a24037 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -1505,6 +1505,19 @@ parse_distinct :: proc(parser: ^Parser, name: token.Token) { _ = 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) { start := advance(parser) explicit_backing := false @@ -1736,6 +1749,10 @@ parse_top_level :: proc(parser: ^Parser) { parse_distinct(parser, name) return } + if operator.kind == .Colon_Colon && current(parser).kind == .Keyword_Alias { + parse_alias(parser, name) + return + } expr := parse_expression(parser) _ = ast.global_id(len(parser.module.globals)) diff --git a/compiler/token/token.odin b/compiler/token/token.odin index e0231e0..e19bf7a 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -54,6 +54,7 @@ Kind :: enum u8 { Keyword_C_Struct, Keyword_Enum, Keyword_Distinct, + Keyword_Alias, Keyword_Import, Keyword_Return, Keyword_Mut, diff --git a/compiler/translatec/translatec.odin b/compiler/translatec/translatec.odin new file mode 100644 index 0000000..6d25a4f --- /dev/null +++ b/compiler/translatec/translatec.odin @@ -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 +} diff --git a/compiler_tests.odin b/compiler_tests.odin index ec481db..bacfbf9 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -17,6 +17,7 @@ import "./compiler/source" import "./compiler/symbol" import "./compiler/target" import "./compiler/token" +import "./compiler/translatec" import "./compiler/types" import "core:fmt" import "core:mem" @@ -5769,3 +5770,83 @@ native_enums_compile_and_run_across_packages :: proc(t: ^testing.T) { state := run_executable(output) 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) +} diff --git a/main.odin b/main.odin index f9c6aaf..9077fc5 100644 --- a/main.odin +++ b/main.odin @@ -4,6 +4,7 @@ import "./compiler" import "./compiler/cimport" import "./compiler/linker" import "./compiler/target" +import "./compiler/translatec" import "core:fmt" import "core:os/os2" @@ -85,9 +86,67 @@ print_usage :: proc() { fmt.eprintln( "usage: brolang -o [--target aarch64-macos] [--c-link | --c-library-path | --c-library | --c-include-path | --c-define ]...", ) + fmt.eprintln( + " brolang translate-c [--target aarch64-macos] [--c-include-path | --c-define ]...", + ) +} + +// 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() { + if len(os2.args) >= 2 && os2.args[1] == "translate-c" { + os2.exit(run_translate_c(os2.args)) + } options, valid := parse_cli_args(os2.args) if !valid { print_usage()