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