# Copyright 2025 DeepMind Technologies Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Utility functions for code generation.""" import os from typing import Union from introspect import ast_nodes from wasm.codegen.generators import constants def write_to_file(filepath: str, content: str) -> None: """Writes content to a file.""" output_dir = os.path.dirname(filepath) try: if output_dir: os.makedirs(output_dir, exist_ok=True) with open(filepath, "w") as f: chars = f.write(content) print(f"wrote {chars} characters to file '{filepath}'") except IOError as e: print(f"Error writing to output file: {filepath} - {e}") def decapitalize(input_string: str) -> str: """Lowercases the first letter of a string.""" return input_string[:1].lower() + input_string[1:] def capitalize(input_string: str) -> str: """Uppercases the first letter of a string.""" return input_string[:1].upper() + input_string[1:] def replace_lines_containing_marker( lines: list[str], marker: str, content: list[str], ) -> list[str]: """Replaces lines containing a specific marker with new content.""" for i, line in enumerate(lines): if marker in line: indent = line[: len(line) - len(line.lstrip(" "))] replacement_lines = [] for text in content: if text.strip(): # Prepend indent to ensure the first replacement line matches the # indentation of the marker and also ensure that text containing # newlines is also indented correctly. # TODO(matijak): This is working around an upstream problem, we should # make it a precondition that content elements do not contain newlines # and fix callers to ensure that. replacement_lines.append( indent + text.replace("\n", f"\n{indent}") + "\n" ) return lines[:i] + replacement_lines + lines[i + 1 :] return lines def get_pointer_return_inner_value_type( func: ast_nodes.FunctionDecl, ) -> ast_nodes.ValueType | None: if not isinstance(func.return_type, ast_nodes.PointerType): return None if not isinstance(func.return_type.inner_type, ast_nodes.ValueType): return None return func.return_type.inner_type def get_inner_value_type( param: ast_nodes.FunctionParameterDecl, ) -> ast_nodes.ValueType | None: if not isinstance(param.type, (ast_nodes.PointerType, ast_nodes.ArrayType)): return None if not isinstance(param.type.inner_type, ast_nodes.ValueType): return None return param.type.inner_type def is_struct_value_type( t: Union[ast_nodes.ValueType, ast_nodes.ArrayType, ast_nodes.PointerType], ) -> bool: """Checks if a type is a struct passed by value.""" if isinstance(t, ast_nodes.ValueType): return ( t.name not in constants.PRIMITIVE_TYPES and t.name != "void" and not t.name.startswith("mjf") ) return False def should_be_wrapped(func: ast_nodes.FunctionDecl) -> bool: """Checks if a MuJoCo function needs a wrapper function.""" if get_pointer_return_inner_value_type(func): return True if is_struct_value_type(func.return_type): return True for param in func.parameters: if get_inner_value_type(param) or is_struct_value_type(param.type): return True return False def wrapped_struct_name(c_struct_name: str) -> str: """Returns the name of the struct wrapping the given C struct.""" return capitalize(c_struct_name) def wrapped_function_name(func: ast_nodes.FunctionDecl) -> str: """Returns the name of the function wrapping the given C function. Hard-coded wrappers in the template need changing if the implementation of the wrapped name is changed Args: func: The FunctionDecl of function to wrap. """ if should_be_wrapped(func): return f"{func.name}_wrapper" return func.name