Files
brolang/compiler/translatec/translatec.odin
T

760 lines
23 KiB
Odin

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(&registry.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(&registry)
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],
&registry,
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 <stdbool.h>) 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
}