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 "../lexer" import "core:fmt" import "core:mem" import "core:strings" Package_Input :: struct { result: ^cimport.Result, header: string, name: string, } Package_Output :: struct { name: string, source: string, } Declaration_Kind :: enum u8 { Record, Alias, Macro, Function, } Declaration :: struct { name: string, canonical: string, kind: Declaration_Kind, } Declaration_Registry :: struct { lookup: map[string]int, declarations: [dynamic]Declaration, error: string, allocator: mem.Allocator, } init_declaration_registry :: proc(allocator: mem.Allocator) -> Declaration_Registry { registry := Declaration_Registry{allocator=allocator} registry.lookup.allocator = allocator registry.declarations.allocator = allocator return registry } destroy_declaration_registry :: proc(registry: ^Declaration_Registry) { for declaration in registry.declarations { delete(declaration.name, registry.allocator) delete(declaration.canonical, registry.allocator) } delete(registry.error, registry.allocator) delete(registry.lookup) delete(registry.declarations) } write_declaration :: proc( b: ^strings.Builder, registry: ^Declaration_Registry, name: string, kind: Declaration_Kind, actual: string, canonical := "", ) -> (emitted, ok: bool) { if registry == nil { strings.write_string(b, actual) return true, true } comparison := canonical if len(canonical) > 0 else actual if index, found := registry.lookup[name]; found { previous := registry.declarations[index] if previous.kind == kind && previous.canonical == comparison { return false, true } if len(registry.error) == 0 { registry.error = fmt.aprintf( "conflicting generated C declaration '%s'", name, allocator=registry.allocator, ) } return false, false } owned_name := strings.clone(name, registry.allocator) owned_canonical := strings.clone(comparison, registry.allocator) registry.lookup[owned_name] = len(registry.declarations) append(®istry.declarations, Declaration{ name=owned_name, canonical=owned_canonical, kind=kind, }) strings.write_string(b, actual) return true, true } emit :: proc(result: ^cimport.Result, header: string, allocator := context.allocator) -> string { record_names := record_name_table(result, allocator) defer destroy_record_name_table(record_names, allocator) output, ok := emit_with_record_names(result, header, record_names, nil, allocator) assert(ok) return output } destroy_package_outputs :: proc(outputs: []Package_Output, allocator := context.allocator) { for output in outputs { delete(output.name, allocator) delete(output.source, allocator) } delete(outputs, allocator) } emit_package :: proc(inputs: []Package_Input, allocator := context.allocator) -> ([]Package_Output, string) { registry := init_declaration_registry(allocator) defer destroy_declaration_registry(®istry) tables := make([][]string, len(inputs), allocator) defer { for table in tables { destroy_record_name_table(table, allocator) } delete(tables, allocator) } for input, index in inputs { tables[index] = record_name_table(input.result, allocator, input.name) } identities: map[string]string identities.allocator = allocator defer delete(identities) for input, input_index in inputs { for record, record_index in input.result.records { if len(record.identity) == 0 { continue } if canonical, found := identities[record.identity]; found { delete(tables[input_index][record_index], allocator) tables[input_index][record_index] = strings.clone(canonical, allocator) } else { identities[record.identity] = tables[input_index][record_index] } } } outputs: [dynamic]Package_Output outputs.allocator = allocator for input, index in inputs { source, ok := emit_with_record_names( input.result, input.header, tables[index], ®istry, allocator, ) if !ok { delete(source, allocator) destroy_package_outputs(outputs[:], allocator) return nil, strings.clone(registry.error, allocator) } append(&outputs, Package_Output{ name=strings.clone(input.name, allocator), source=source, }) } return outputs[:], "" } emit_with_record_names :: proc( result: ^cimport.Result, header: string, record_names: []string, registry: ^Declaration_Registry, allocator: mem.Allocator, ) -> (string, bool) { b := strings.builder_make(allocator) fmt.sbprintf(&b, "# generated by brolang translate-c from %s\n\n", header) if !emit_records(&b, result, record_names, registry) || !emit_aliases(&b, result, record_names, registry) || !emit_macros(&b, result, record_names, registry) || !emit_functions(&b, result, record_names, registry) { return strings.to_string(b), false } emit_variables(&b, result) emit_unsupported(&b, result) return strings.to_string(b), true } // 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, prefix := "") -> []string { names := make([]string, len(result.records), allocator) for record, idx in result.records { if binding_identifier(record.name) { if record_name_conflicts(result, idx, record.name) { fragment := safe_name_fragment(prefix, context.temp_allocator) if len(fragment) > 0 { names[idx] = fmt.aprintf("__c_%s_%s_record", fragment, record.name, allocator=allocator) } else { names[idx] = fmt.aprintf("__c_%s_record", record.name, allocator=allocator) } } else { names[idx] = strings.clone(record.name, allocator) } } else if len(prefix) > 0 { fragment := safe_name_fragment(prefix, context.temp_allocator) names[idx] = fmt.aprintf("__c_%s_record_%d", fragment, idx, allocator=allocator) } 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 !binding_identifier(result.records[target.record].name) { delete(names[target.record], allocator) names[target.record] = strings.clone(alias.name, allocator) } } return names } binding_identifier :: proc(value: string) -> bool { if len(value) == 0 || lexer.keyword_kind(value) != .Identifier { return false } for byte, index in transmute([]byte)value { letter := byte == '_' || byte >= 'a' && byte <= 'z' || byte >= 'A' && byte <= 'Z' if !letter && (index == 0 || byte < '0' || byte > '9') { return false } } return true } record_name_conflicts :: proc(result: ^cimport.Result, record_index: int, name: string) -> bool { for function in result.functions { if function.name == name && len(function.reason) == 0 && (len(function.link_name) == 0 || function.link_name == function.name) { return true } } for macro in result.macros { if macro.name == name && len(macro.reason) == 0 && lexer.keyword_kind(macro.name) == .Identifier { return true } } for alias in result.aliases { if alias.name != name || len(alias.reason) > 0 || alias.type == cimport.INVALID_TYPE || int(alias.type) < 0 || int(alias.type) >= len(result.types) { continue } target := result.types[alias.type] if target.kind != .Record || int(target.record) != record_index { return true } } return false } destroy_record_name_table :: proc(names: []string, allocator: mem.Allocator) { for name in names { delete(name, allocator) } delete(names, allocator) } safe_name_fragment :: proc(value: string, allocator: mem.Allocator) -> string { b := strings.builder_make(allocator) for byte in transmute([]byte)value { if byte >= 'a' && byte <= 'z' || byte >= 'A' && byte <= 'Z' || byte >= '0' && byte <= '9' || byte == '_' { strings.write_byte(&b, byte) } else { strings.write_byte(&b, '_') } } return strings.to_string(b) } // 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_Bool: strings.write_string(b, "bool") 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 ") } if int(item.child) >= 0 && int(item.child) < len(result.types) && result.types[item.child].kind == .Void { strings.write_string(b, "anyopaque") } else { 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: // Function-pointer types carry no parameter names, so every slot renders `_`. render_c_func(b, result, item.params, nil, 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(name T0, ...) R`, using the real C parameter name // when `names` provides one and `_` (the sink) otherwise. Names do not affect type // identity; the parser requires a name slot, so unnamed params use `_`. render_c_func :: proc( b: ^strings.Builder, result: ^cimport.Result, params: []cimport.Type_Id, names: []string, 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, ", ") } name := names[index] if index < len(names) else "" fmt.sbprintf(b, "%s ", safe_param_name(name)) 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, registry: ^Declaration_Registry, ) -> bool { wrote := false for record, idx in result.records { name := record_names[idx] representable := record_has_native_spelling(result, u32(idx)) if record.kind == .Union { fmt.sbprintf(b, "# unsupported in bindings: C union '%s' has no native spelling\n", name) wrote = true } else if record.complete && len(record.reason) == 0 && !representable { fmt.sbprintf(b, "# unsupported in bindings: C record '%s' contains an unsupported field type\n", name) wrote = true } if !representable { // opaque / pointer-only struct text := fmt.tprintf("%s :: opaque\n", name) canonical := text if len(record.identity) > 0 { canonical = fmt.tprintf("%s\n%s", record.identity, text) } emitted, ok := write_declaration(b, registry, name, .Record, text, canonical) if !ok { return false } wrote = wrote || emitted continue } declaration := strings.builder_make(context.temp_allocator) fmt.sbprintf(&declaration, "%s :: c_struct {{\n", name) for field in record.fields { fmt.sbprintf(&declaration, "\t%s ", field.name) render_type(&declaration, result, field.type, record_names) strings.write_byte(&declaration, '\n') } strings.write_string(&declaration, "}\n") text := strings.to_string(declaration) canonical := text if len(record.identity) > 0 { canonical = fmt.tprintf("%s\n%s", record.identity, text) } emitted, ok := write_declaration(b, registry, name, .Record, text, canonical) if !ok { return false } wrote = wrote || emitted } if wrote { strings.write_byte(b, '\n') } return true } emit_aliases :: proc( b: ^strings.Builder, result: ^cimport.Result, record_names: []string, registry: ^Declaration_Registry, ) -> bool { 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 } if !type_has_native_spelling(result, alias.type) { fmt.sbprintf(b, "# unsupported in bindings: typedef '%s' — underlying type has no native spelling\n", alias.name) 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 := result.records[target.record] if record_names[target.record] == alias.name || record.name == alias.name { continue } } } declaration := strings.builder_make(context.temp_allocator) fmt.sbprintf(&declaration, "%s :: alias ", alias.name) render_type(&declaration, result, alias.type, record_names) strings.write_byte(&declaration, '\n') text := strings.to_string(declaration) emitted, ok := write_declaration(b, registry, alias.name, .Alias, text) if !ok { return false } wrote = wrote || emitted } if wrote { strings.write_byte(b, '\n') } return true } emit_macros :: proc( b: ^strings.Builder, result: ^cimport.Result, record_names: []string, registry: ^Declaration_Registry, ) -> bool { 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 } // A macro whose name collides with a brolang keyword (e.g. `true`/`false` // from ) can't be a binding name; emitting it is a parse error. if lexer.keyword_kind(macro.name) != .Identifier { fmt.sbprintf(b, "# unsupported in bindings: macro '%s' — name is a brolang keyword\n", macro.name) wrote = true continue } if macro.aggregate { declaration := strings.builder_make(context.temp_allocator) binding := emit_aggregate_macro(&declaration, result, macro, record_names) text := strings.to_string(declaration) if binding { emitted, ok := write_declaration(b, registry, macro.name, .Macro, text) if !ok { return false } wrote = wrote || emitted } else { strings.write_string(b, text) wrote = true } continue } declaration := strings.builder_make(context.temp_allocator) strings.write_string(&declaration, macro.name) if macro_scalar_kind(result, macro.type) { strings.write_byte(&declaration, ' ') render_type(&declaration, result, macro.type, record_names) } strings.write_string(&declaration, " :: ") render_macro_value(&declaration, macro.value) strings.write_byte(&declaration, '\n') text := strings.to_string(declaration) emitted, ok := write_declaration(b, registry, macro.name, .Macro, text) if !ok { return false } wrote = wrote || emitted } if wrote { strings.write_byte(b, '\n') } return true } emit_aggregate_macro :: proc( b: ^strings.Builder, result: ^cimport.Result, macro: cimport.Macro_Constant, record_names: []string, ) -> bool { 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_has_native_spelling(result, u32(ridx)) { 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 true } } } fmt.sbprintf(b, "# unsupported in bindings: aggregate macro '%s' has no native spelling\n", macro.name) return false } emit_functions :: proc( b: ^strings.Builder, result: ^cimport.Result, record_names: []string, registry: ^Declaration_Registry, ) -> bool { 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 !function_has_native_spelling(result, function) { fmt.sbprintf(b, "# unsupported in bindings: function '%s' — signature has no native spelling\n", function.name) 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 } declaration := strings.builder_make(context.temp_allocator) fmt.sbprintf(&declaration, "%s ", function.name) render_c_func(&declaration, result, function.params, function.param_names, function.result, function.variadic, record_names) strings.write_byte(&declaration, '\n') canonical := strings.builder_make(context.temp_allocator) fmt.sbprintf(&canonical, "%s ", function.name) render_c_func(&canonical, result, function.params, nil, function.result, function.variadic, record_names) strings.write_byte(&canonical, '\n') emitted, ok := write_declaration( b, registry, function.name, .Function, strings.to_string(declaration), strings.to_string(canonical), ) if !ok { return false } wrote = wrote || emitted } if wrote { strings.write_byte(b, '\n') } return true } 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") } } // safe_param_name returns the C parameter name when it is a usable brolang // identifier, else `_` (the sink): empty names (unnamed C params) and names that // collide with a brolang keyword (e.g. legal C `int f(int and)`) both become `_`. safe_param_name :: proc(name: string) -> string { if len(name) > 0 && lexer.keyword_kind(name) == .Identifier { return name } return "_" } 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 } function_has_native_spelling :: proc(result: ^cimport.Result, function: cimport.Function) -> bool { for param in function.params { if !type_has_native_spelling(result, param) { return false } } return type_has_native_spelling(result, function.result, allow_void=true) } record_has_native_spelling :: proc(result: ^cimport.Result, index: u32, depth := 0) -> bool { if depth > 64 || int(index) < 0 || int(index) >= len(result.records) { return false } record := result.records[index] if record.kind == .Union || !record.complete || len(record.reason) > 0 || len(record.fields) == 0 { return false } for field in record.fields { if !type_has_native_spelling(result, field.type, depth=depth+1) { return false } } return true } type_has_native_spelling :: proc( result: ^cimport.Result, id: cimport.Type_Id, allow_void := false, depth := 0, ) -> bool { if depth > 64 || id == cimport.INVALID_TYPE || int(id) < 0 || int(id) >= len(result.types) { return false } item := result.types[id] switch item.kind { case .Void: return allow_void case .C_Bool, .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 case .Pointer: if item.child == cimport.INVALID_TYPE || int(item.child) < 0 || int(item.child) >= len(result.types) { return false } child := result.types[item.child] if child.kind == .Void || child.kind == .Record { return true } return type_has_native_spelling(result, item.child, allow_void=true, depth=depth+1) case .Array: return type_has_native_spelling(result, item.child, depth=depth+1) case .Function: for param in item.params { if !type_has_native_spelling(result, param, depth=depth+1) { return false } } return type_has_native_spelling(result, item.child, allow_void=true, depth=depth+1) case .Record: return record_has_native_spelling(result, item.record, depth+1) case .Invalid: return false } 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 }