clay/generator/generators/odin_generator.py
Harrison Lambeth f4933c6669 small refactor
2025-02-10 23:14:44 -07:00

316 lines
13 KiB
Python

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}")