from pathlib import Path import logging from parser import ExtractedSymbolType from generators.base_generator import BaseGenerator logger = logging.getLogger(__name__) def get_common_prefix(keys: list[str]) -> str: # find a prefix that's shared between all keys prefix = "" for i in range(min(map(len, keys))): if len(set(key[i] for key in keys)) > 1: break prefix += keys[0][i] return prefix def snake_case_to_pascal_case(snake_case: str) -> str: return ''.join(word.lower().capitalize() for word in snake_case.split('_')) SYMBOL_NAME_OVERRIDES = { 'Clay_TextElementConfigWrapMode': 'TextWrapMode', 'Clay_Border': 'BorderData', 'Clay_SizingMinMax': 'SizingConstraintsMinMax', } SYMBOL_COMPLETE_OVERRIDES = { 'Clay_RenderCommandArray': 'ClayArray(RenderCommand)', 'Clay_Context': 'Context', 'Clay_ElementConfig': None, # 'Clay_SetQueryScrollOffsetFunction': None, } # These enums should have output binding members that are PascalCase instead of UPPER_SNAKE_CASE. ENUM_MEMBER_PASCAL = { 'Clay_RenderCommandType', 'Clay_TextElementConfigWrapMode', 'Clay__ElementConfigType', } ENUM_MEMBER_OVERRIDES = { 'Clay__ElementConfigType': { 'CLAY__ELEMENT_CONFIG_TYPE_BORDER_CONTAINER': 'Border', 'CLAY__ELEMENT_CONFIG_TYPE_FLOATING_CONTAINER': 'Floating', 'CLAY__ELEMENT_CONFIG_TYPE_SCROLL_CONTAINER': 'Scroll', } } ENUM_ADDITIONAL_MEMBERS = { 'Clay__ElementConfigType': { 'Id': 65, 'Layout': 66, } } TYPE_MAPPING = { '*char': '[^]c.char', 'const *char': '[^]c.char', '*void': 'rawptr', 'bool': 'bool', 'float': 'c.float', 'uint16_t': 'u16', 'uint32_t': 'u32', 'int32_t': 'c.int32_t', 'uintptr_t': 'rawptr', 'intptr_t': 'rawptr', 'void': 'void', } STRUCT_TYPE_OVERRIDES = { 'Clay_Arena': { 'nextAllocation': 'uintptr', 'capacity': 'uintptr', }, 'Clay_SizingAxis': { 'size': 'SizingConstraints', }, "Clay_RenderCommand": { "zIndex": 'i32', }, } STRUCT_MEMBER_OVERRIDES = { 'Clay_ErrorHandler': { 'errorHandlerFunction': 'handler', }, 'Clay_SizingAxis': { 'size': 'constraints', }, } STRUCT_OVERRIDE_AS_FIXED_ARRAY = { 'Clay_Color', 'Clay_Vector2', } FUNCTION_PARAM_OVERRIDES = { 'Clay_SetCurrentContext': { 'context': 'ctx', }, } FUNCTION_TYPE_OVERRIDES = { 'Clay_CreateArenaWithCapacityAndMemory': { 'offset': '[^]u8', }, 'Clay_SetMeasureTextFunction': { 'userData': 'uintptr', }, 'Clay_RenderCommandArray_Get': { 'index': 'i32', }, "Clay__AttachElementConfig": { "config": 'rawptr', }, } class OdinGenerator(BaseGenerator): def generate(self) -> None: self.generate_structs() self.generate_enums() self.generate_functions() odin_template_path = Path(__file__).parent / 'odin' / 'clay.template.odin' with open(odin_template_path, 'r') as f: template = f.read() self.output_content['clay.odin'] = ( template .replace('{{structs}}', '\n'.join(self.output_content['struct'])) .replace('{{enums}}', '\n'.join(self.output_content['enum'])) .replace('{{public_functions}}', '\n'.join(self.output_content['public_function'])) .replace('{{private_functions}}', '\n'.join(self.output_content['private_function'])) .splitlines() ) del self.output_content['struct'] del self.output_content['enum'] del self.output_content['private_function'] del self.output_content['public_function'] def get_symbol_name(self, symbol: str) -> str: if symbol in SYMBOL_NAME_OVERRIDES: return SYMBOL_NAME_OVERRIDES[symbol] symbol_type = self.get_symbol_type(symbol) base_name = symbol.removeprefix('Clay_') if symbol_type == 'enum': return base_name.removeprefix('_') # Clay_ and Clay__ are exported as public types. elif symbol_type == 'struct': return base_name elif symbol_type == 'function': return base_name raise ValueError(f'Unknown symbol: {symbol}') def format_type(self, type: ExtractedSymbolType) -> str: if isinstance(type, str): return type parameter_strs = [] for param_name, param_type in type['params']: parameter_strs.append(f"{param_name}: {self.format_type(param_type or 'unknown')}") return_type_str = '' if type['return_type'] is not None and type['return_type'] != 'void': return_type_str = ' -> ' + self.format_type(type['return_type']) return f"proc \"c\" ({', '.join(parameter_strs)}){return_type_str}" def resolve_binding_type(self, symbol: str, member: str | None, member_type: ExtractedSymbolType | None, type_overrides: dict[str, dict[str, str]]) -> str | None: if isinstance(member_type, str): if member_type in SYMBOL_COMPLETE_OVERRIDES: return SYMBOL_COMPLETE_OVERRIDES[member_type] if symbol in type_overrides and member in type_overrides[symbol]: return type_overrides[symbol][member] if member_type in TYPE_MAPPING: return TYPE_MAPPING[member_type] if member_type and self.has_symbol(member_type): return self.get_symbol_name(member_type) if member_type and member_type.startswith('*'): result = self.resolve_binding_type(symbol, member, member_type[1:], type_overrides) if result: return f"^{result}" return None if member_type is None: return None resolved_parameters = [] for param_name, param_type in member_type['params']: resolved_param = self.resolve_binding_type(symbol, param_name, param_type, type_overrides) if resolved_param is None: return None resolved_parameters.append((param_name, resolved_param)) resolved_return_type = self.resolve_binding_type(symbol, None, member_type['return_type'], type_overrides) if resolved_return_type is None: return None return self.format_type({ "params": resolved_parameters, "return_type": resolved_return_type, }) def generate_structs(self) -> None: for struct, struct_data in sorted(self.extracted_symbols.structs.items(), key=lambda x: x[0]): members = struct_data['attrs'] if not struct.startswith('Clay_'): continue if struct in SYMBOL_COMPLETE_OVERRIDES: continue binding_name = self.get_symbol_name(struct) if binding_name.startswith('_'): continue if struct in STRUCT_OVERRIDE_AS_FIXED_ARRAY: array_size = len(members) first_elem = list(members.values())[0] array_type = None if 'type' in first_elem: array_type = first_elem['type'] if array_type in TYPE_MAPPING: array_binding_type = TYPE_MAPPING[array_type] elif array_type and self.has_symbol(self.format_type(array_type)): array_binding_type = self.get_symbol_name(self.format_type(array_type)) else: self._write('struct', f"// {struct} ({array_type}) - has no mapping") continue self._write('struct', f"// {struct} (overridden as fixed array)") self._write('struct', f"{binding_name} :: [{array_size}]{array_binding_type}") self._write('struct', "") continue raw_union = ' #raw_union' if struct_data.get('is_union', False) else '' self._write('struct', f"// {struct}") self._write('struct', f"{binding_name} :: struct{raw_union} {{") for member, member_info in members.items(): if struct in STRUCT_TYPE_OVERRIDES and member in STRUCT_TYPE_OVERRIDES[struct]: member_type = 'unknown' elif not 'type' in member_info: self._write('struct', f" // {member} (unknown type)") continue else: member_type = member_info['type'] binding_member_name = member if struct in STRUCT_MEMBER_OVERRIDES and member in STRUCT_MEMBER_OVERRIDES[struct]: binding_member_name = STRUCT_MEMBER_OVERRIDES[struct][member] member_binding_type = self.resolve_binding_type(struct, member, member_type, STRUCT_TYPE_OVERRIDES) if member_binding_type is None: self._write('struct', f" // {binding_member_name} ({member_type}) - has no mapping") continue self._write('struct', f" {binding_member_name}: {member_binding_type}, // {member} ({member_type})") self._write('struct', "}") self._write('struct', '') def generate_enums(self) -> None: for enum, members in sorted(self.extracted_symbols.enums.items(), key=lambda x: x[0]): if not enum.startswith('Clay_'): continue if enum in SYMBOL_COMPLETE_OVERRIDES: continue binding_name = self.get_symbol_name(enum) common_member_prefix = get_common_prefix(list(members.keys())) self._write('enum', f"// {enum}") self._write('enum', f"{binding_name} :: enum EnumBackingType {{") for member in members: if enum in ENUM_MEMBER_OVERRIDES and member in ENUM_MEMBER_OVERRIDES[enum]: binding_member_name = ENUM_MEMBER_OVERRIDES[enum][member] else: binding_member_name = member.removeprefix(common_member_prefix) if enum in ENUM_MEMBER_PASCAL: binding_member_name = snake_case_to_pascal_case(binding_member_name) if members[member] is not None: self._write('enum', f" {binding_member_name} = {members[member]}, // {member}") else: self._write('enum', f" {binding_member_name}, // {member}") if enum in ENUM_ADDITIONAL_MEMBERS: self._write('enum', ' // Odin specific enum types') for member, value in ENUM_ADDITIONAL_MEMBERS[enum].items(): self._write('enum', f" {member} = {value},") self._write('enum', "}") self._write('enum', '') def generate_functions(self) -> None: for function, function_info in sorted(self.extracted_symbols.functions.items(), key=lambda x: x[0]): if not function.startswith('Clay_'): continue if function in SYMBOL_COMPLETE_OVERRIDES: continue is_private = function.startswith('Clay__') write_to = 'private_function' if is_private else 'public_function' binding_name = self.get_symbol_name(function) return_type = function_info['return_type'] binding_return_type = self.resolve_binding_type(function, None, return_type, {}) if binding_return_type is None: self._write(write_to, f" // {function} ({return_type}) - has no mapping") continue skip = False binding_params = [] for param_name, param_type in function_info['params']: binding_param_name = param_name if function in FUNCTION_PARAM_OVERRIDES and param_name in FUNCTION_PARAM_OVERRIDES[function]: binding_param_name = FUNCTION_PARAM_OVERRIDES[function][param_name] binding_param_type = self.resolve_binding_type(function, param_name, param_type, FUNCTION_TYPE_OVERRIDES) if binding_param_type is None: skip = True binding_params.append(f"{binding_param_name}: {binding_param_type}") if skip: self._write(write_to, f" // {function} - has no mapping") continue binding_params_str = ', '.join(binding_params) return_str = f" -> {binding_return_type}" if binding_return_type != 'void' else '' self._write(write_to, f" {binding_name} :: proc({binding_params_str}){return_str} --- // {function}")